Skip to content

Commit f227960

Browse files
authored
Merge pull request #716 from devforth/security-fixes
Security fixes
2 parents e6cdd83 + 7ef1132 commit f227960

29 files changed

Lines changed: 672 additions & 41 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules
2+
.env
23
{{#if sqliteFile}}
34
{{ sqliteFile }}
45
{{/if}}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Secrets live in the gitignored .env; this committed file only lists which ones each developer must create.
2+
# create-app already wrote a random ADMINFORTH_SECRET into .env on the machine that scaffolded the project.
3+
# On a fresh clone, generate your own — see "First-time setup" in README.md (openssl rand -hex 32).
4+
ADMINFORTH_SECRET=
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
# Add only sensitive local environment variables here; non-sensitive local variables should go to .env.local
1+
# Add only sensitive local environment variables here; non-sensitive local variables should go to .env.local
2+
# This file is gitignored. Each developer and each deployment needs its own ADMINFORTH_SECRET.
3+
4+
ADMINFORTH_SECRET={{{adminforthSecret}}}

‎adminforth/commands/createApp/templates/.env.local.hbs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Add only non-sensitive local environment variables here so all team members can use them with minimal setup
22
# For sensitive local environment variables, use .env and explain to team members how to set them, ideally with a .env.example
33

4-
ADMINFORTH_SECRET=123
54
NODE_ENV=development
65
DEBUG_LEVEL=info
76
AF_DEBUG_LEVEL=info

‎adminforth/commands/createApp/templates/adminuser.ts.hbs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default {
5757
{
5858
name: 'role',
5959
type: AdminForthDataTypes.STRING,
60-
required: true
60+
required: true,
6161
enum: [
6262
{ value: 'superadmin', label: 'Super Admin' },
6363
{ value: 'user', label: 'User' },

‎adminforth/commands/createApp/templates/readme.md.hbs‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Install dependencies:
77
```
88

99
{{#if adminUserTableInstructions}}
10-
Prepare the admin users table in your existing database before starting the app. AdminForth uses this table for back-office authentication, and your own migration tool should own this schema change. The schema below is only an example:
10+
Create the admin users table in your database before starting the app: AdminForth needs it for back-office authentication, and no Prisma migrations were generated for this project.
1111

1212
{{{adminUserTableInstructions}}}
1313

@@ -23,6 +23,13 @@ Create the initial migration and apply it to the database:
2323
```
2424
{{/if}}
2525

26+
First-time setup on a fresh clone: `create-app` wrote a random `ADMINFORTH_SECRET` into `.env`, but that file is gitignored, so every developer and every deployment creates their own (see `.env.example`). The command below only creates `.env` when it does not exist yet:
27+
28+
```bash
29+
# On Windows (PowerShell): node -e "require('fs').writeFileSync('.env', 'ADMINFORTH_SECRET=' + require('crypto').randomBytes(32).toString('hex') + '\n', { flag: 'wx' })"
30+
[ -f .env ] || (umask 077; echo "ADMINFORTH_SECRET=$(openssl rand -hex 32)" > .env)
31+
```
32+
2633
Start the server:
2734

2835
```bash
@@ -48,8 +55,12 @@ Your colleagues will need to pull the changes and run `{{packageManagerRun}} mig
4855
You have Dockerfile ready for production deployment. You can test the build with:
4956

5057
```bash
58+
# Generate a signing key once and keep it in your secret store (never commit it).
59+
# On Windows without openssl: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
60+
export ADMINFORTH_SECRET="$(openssl rand -hex 32)"
61+
5162
docker build -t {{appName}}-image .
52-
docker run -p 3500:3500 -e ADMINFORTH_SECRET=123 {{#if sqliteFile}}-v $(pwd)/db:/code/db {{/if}}{{appName}}-image
63+
docker run -p 3500:3500 -e ADMINFORTH_SECRET="$ADMINFORTH_SECRET" {{#if sqliteFile}}-v $(pwd)/db:/code/db {{/if}}{{appName}}-image
5364
```
5465

5566
To set non-sensitive environment variables in production, use `.env.prod` file.

‎adminforth/commands/createApp/utils.js‎

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Listr } from 'listr2'
99
import { fileURLToPath, pathToFileURL } from 'url';
1010
import {ConnectionString} from 'connection-string';
1111
import { exec } from 'child_process';
12+
import crypto from 'crypto';
1213

1314
import Handlebars from 'handlebars';
1415
import { promisify } from 'util';
@@ -152,6 +153,10 @@ async function inspectDatabaseCleanState(options) {
152153
const provider = detectDbProvider(connectionString.protocol);
153154
const dbConnString = connectionString.toString();
154155

156+
// `dbInspected` records whether the answer below is a real observation or a fallback
157+
// (connector unavailable), so the caller can avoid claiming the database is empty.
158+
options.dbInspected = true;
159+
155160
// Fast path for SQLite: a missing file is by definition a brand new database.
156161
// Avoid connecting (which would otherwise create the file) and avoid pulling the
157162
// connector for the common new-project case.
@@ -171,6 +176,7 @@ async function inspectDatabaseCleanState(options) {
171176
// the normal Prisma flow stays available instead of failing create-app.
172177
console.log(chalk.yellow(`\n⚠️ Could not load the database connector to inspect the database (${error.message}). Continuing as a new database.`));
173178
options.existingDb = false;
179+
options.dbInspected = false;
174180
return;
175181
}
176182

@@ -180,6 +186,7 @@ async function inspectDatabaseCleanState(options) {
180186
// Connector predates the isDatabaseEmpty() probe (version skew); cannot
181187
// determine emptiness, so assume a new database and keep the Prisma flow.
182188
options.existingDb = false;
189+
options.dbInspected = false;
183190
return;
184191
}
185192

@@ -247,6 +254,29 @@ export async function promptForMissingOptions(options) {
247254

248255
await inspectDatabaseCleanState(resolvedOptions);
249256

257+
// Say which path was taken: the decision is made by inspecting the database, not by whether --db was passed
258+
const prismaCapable = isPrismaMigrationDbUrl(resolvedOptions.db);
259+
const adminUserSql = generateAdminUserTableInstructions(detectDbProvider(parseConnectionString(resolvedOptions.db).protocol));
260+
const sqlNote = adminUserSql
261+
? 'The generated README and the instructions printed at the end include the SQL for the required adminuser table.'
262+
: 'No table has to be created up front for this database.';
263+
const willAskAboutPrisma = resolvedOptions.includePrismaMigrations === undefined && prismaCapable && !resolvedOptions.existingDb;
264+
265+
if (!resolvedOptions.dbInspected) {
266+
console.log(chalk.yellow('\n🗄️ Continuing as a new database because it could not be inspected.' +
267+
(prismaCapable ? ' Prisma migrations are therefore not offered by default; answer Yes below only if it is in fact empty.' : '') +
268+
` ${sqlNote}`));
269+
} else if (resolvedOptions.existingDb) {
270+
console.log(chalk.cyan(`\n🗄️ The database already contains data, so no Prisma migrations will be generated for it. ${sqlNote}`));
271+
} else if (willAskAboutPrisma) {
272+
console.log(chalk.cyan('\n🗄️ The database is empty. Answer Yes below to let AdminForth manage its schema with Prisma migrations, ' +
273+
`or No to manage it yourself${adminUserSql ? ' (you will get the SQL for the required adminuser table instead)' : ''}.`));
274+
} else if (prismaCapable && resolvedOptions.includePrismaMigrations) {
275+
console.log(chalk.cyan('\n🗄️ The database is empty; AdminForth will manage its schema with Prisma migrations.'));
276+
} else {
277+
console.log(chalk.cyan(`\n🗄️ The database is empty and no Prisma migrations will be generated. ${sqlNote}`));
278+
}
279+
250280
if (
251281
resolvedOptions.includePrismaMigrations === undefined &&
252282
isPrismaMigrationDbUrl(resolvedOptions.db) &&
@@ -260,7 +290,9 @@ export async function promptForMissingOptions(options) {
260290
{ name: 'Yes', value: true },
261291
{ name: 'No', value: false },
262292
],
263-
default: true,
293+
// only recommend Prisma when the database was actually observed to be empty: on a database
294+
// we could not inspect, pressing Enter must not scaffold migrations over existing data
295+
default: resolvedOptions.dbInspected,
264296
}]);
265297
resolvedOptions.includePrismaMigrations = prismaAnswer.includePrismaMigrations;
266298
} else {
@@ -432,7 +464,6 @@ async function scaffoldProject(ctx, options, cwd) {
432464
prismaDbUrlProd,
433465
appName,
434466
provider,
435-
existingDb: options.existingDb,
436467
nodeMajor: parseInt(process.versions.node.split('.')[0], 10),
437468
sqliteFile: connectionString.protocol.startsWith('sqlite') ? connectionString.host : null,
438469
});
@@ -454,9 +485,9 @@ function getPackageManagerTemplateData(useNpm, nodeMajor) {
454485
};
455486
}
456487

457-
async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations, options) {
488+
export async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations, options) {
458489
const {
459-
dbUrl, prismaDbUrl, appName, provider, existingDb, nodeMajor,
490+
dbUrl, prismaDbUrl, appName, provider, nodeMajor,
460491
dbUrlProd, prismaDbUrlProd, sqliteFile
461492
} = options;
462493
const packageManagerTemplateData = getPackageManagerTemplateData(useNpm, nodeMajor);
@@ -505,8 +536,9 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations,
505536
prismaDbUrl: resolvedPrismaDbUrl,
506537
appName,
507538
sqliteFile,
508-
existingDb,
509-
adminUserTableInstructions: existingDb ? generateAdminUserTableInstructions(provider) : null,
539+
// whenever Prisma migrations will not manage the schema (same rule as skipPrismaSetup in scaffoldProject),
540+
// the user has to create the adminuser table themselves
541+
adminUserTableInstructions: (!includePrismaMigrations || !prismaDbUrl) ? generateAdminUserTableInstructions(provider) : null,
510542
},
511543
},
512544
{
@@ -540,10 +572,17 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations,
540572
data: {},
541573
},
542574
{
543-
// We'll write .env using the same content as .env.sample
575+
// gitignored: holds the JWT signing key, unique per developer and per deployment
544576
src: '.env.hbs',
545577
dest: '.env',
546-
data: { dbUrl, prismaDbUrl: resolvedPrismaDbUrl },
578+
data: { adminforthSecret: crypto.randomBytes(32).toString('hex') },
579+
mode: 0o600,
580+
},
581+
{
582+
// committed: tells the next developer which secrets to create locally
583+
src: '.env.example.hbs',
584+
dest: '.env.example',
585+
data: {},
547586
},
548587
{
549588
src: 'adminuser.ts.hbs',
@@ -635,7 +674,11 @@ async function writeTemplateFiles(dirname, cwd, useNpm, includePrismaMigrations,
635674
...packageManagerTemplateData,
636675
...task.data,
637676
});
638-
await fs.promises.writeFile(destPath, compiled);
677+
await fs.promises.writeFile(destPath, compiled, task.mode ? { mode: task.mode } : undefined);
678+
if (task.mode) {
679+
// writeFile's mode is masked by the umask and ignored on overwrite; enforce it unconditionally
680+
await fs.promises.chmod(destPath, task.mode);
681+
}
639682
}
640683
}
641684
}
@@ -678,10 +721,10 @@ async function installDependenciesNpm(ctx, cwd) {
678721
}
679722
}
680723

681-
function generateFinalInstructionsPnpm(skipPrismaSetup, options) {
724+
export function generateFinalInstructionsPnpm(skipPrismaSetup, options) {
682725
let instruction = '⏭️ Run the following commands to get started:\n';
683726
const provider = detectDbProvider(parseConnectionString(options.db).protocol);
684-
const adminUserTableInstructions = options.existingDb ? generateAdminUserTableInstructions(provider) : null;
727+
const adminUserTableInstructions = skipPrismaSetup ? generateAdminUserTableInstructions(provider) : null;
685728
instruction += `
686729
${chalk.dim('// Go to the project directory')}
687730
${chalk.dim('$')}${chalk.cyan(` cd ${options.appName}`)}\n`;
@@ -706,10 +749,10 @@ function generateFinalInstructionsPnpm(skipPrismaSetup, options) {
706749
return instruction;
707750
}
708751

709-
function generateFinalInstructionsNpm(skipPrismaSetup, options) {
752+
export function generateFinalInstructionsNpm(skipPrismaSetup, options) {
710753
let instruction = '⏭️ Run the following commands to get started:\n';
711754
const provider = detectDbProvider(parseConnectionString(options.db).protocol);
712-
const adminUserTableInstructions = options.existingDb ? generateAdminUserTableInstructions(provider) : null;
755+
const adminUserTableInstructions = skipPrismaSetup ? generateAdminUserTableInstructions(provider) : null;
713756
instruction += `
714757
${chalk.dim('// Go to the project directory')}
715758
${chalk.dim('$')}${chalk.cyan(` cd ${options.appName}`)}\n`;

‎adminforth/documentation/docs/tutorial/001-gettingStarted.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Use this path when you already have a database and your own schema or migrations
3434
npx adminforth create-app --app-name myadmin --db "postgresql://user:password@localhost:5432/dbname"
3535
```
3636

37-
When you provide your own database URL, the CLI treats this as your own database. It does not create Prisma schema or Prisma migration scripts for that database. Instead, the generated project README contains the SQL or schema notes for adding the required `adminuser` table with your own migration tool.
37+
The CLI connects to the database and checks whether it already contains tables — passing `--db` alone does not decide the path. If tables exist, the database is treated as your own: no Prisma schema or migration scripts are generated, and both the CLI output and the generated project README contain the SQL for adding the required `adminuser` table with your own migration tool (MongoDB needs no table up front — the collection is created on first write). If a PostgreSQL, MySQL or SQLite database is still empty, the CLI asks `Include Prisma migrations? >` — answer **No** to keep managing the schema yourself and you get the same `adminuser` SQL; answer **Yes** and AdminForth manages the schema for you (see Path 2). The CLI offers Prisma migrations only for PostgreSQL, MySQL and SQLite, so MongoDB and ClickHouse never get the question and always keep their schema yours. If the CLI cannot load the database connector (for example offline, or the installed connector is too old to inspect a database), it warns, continues as if the database were empty and defaults the question to **No**, so pressing Enter never scaffolds migrations over a database that may already hold data; a database it cannot connect to makes `create-app` fail with the connection error.
3838

3939
After the project is created, navigate into it and generate resources from your existing tables:
4040

@@ -65,12 +65,12 @@ Once the project is created, navigate into its directory:
6565
cd myadmin # or any other name you provided
6666
```
6767

68-
For the new database path, the CLI can scaffold Prisma files and migration scripts for the default SQLite database.
68+
For an empty database (the default SQLite file, or an empty SQLite/PostgreSQL/MySQL database passed with `--db`), the CLI asks `Include Prisma migrations? >`, defaulting to **Yes**. Answer **Yes** to have AdminForth scaffold the Prisma schema and migration scripts; answer **No** to manage the schema yourself — the CLI output and the generated README then contain the SQL for the required `adminuser` table.
6969

7070
CLI options:
7171

7272
* **`--app-name`** - name for your project. Used in `package.json`, `index.ts` branding, etc. Default value: **`adminforth-app`**.
73-
* **`--db`** - database connection string. Currently PostgreSQL, MongoDB, SQLite, MySQL, Clickhouse and Qdrant (read only) are supported. Default value: **`sqlite://.db.sqlite`**
73+
* **`--db`** - database connection string. `create-app` accepts `sqlite://`, `postgresql://`, `mongodb://`, `mysql://` and `clickhouse://` URLs. Default value: **`sqlite://.db.sqlite`**
7474

7575
> ☝️ Database Connection String format:
7676
>
@@ -95,20 +95,21 @@ myadmin/
9595
│ └── tsconfig.json # Tsconfig for Vue project (adds completion for AdminForth core components)
9696
├── resources
9797
│ └── adminuser.ts # Example resource file for users management
98-
├── schema.prisma # Prisma schema file, generated only for the new database path
98+
├── schema.prisma # Prisma schema file, generated only when you include Prisma migrations
9999
├── index.ts # Main entry point: configures AdminForth & starts the server
100100
├── package.json # Project dependencies
101101
├── pnpm-workspace.yaml
102102
├── tsconfig.json # TypeScript configuration
103-
├── .env # Env vars like tokens, secrets that should not be in version control
103+
├── .env # Env vars like tokens, secrets that should not be in version control (ADMINFORTH_SECRET is generated here for you)
104+
├── .env.example # Committed template listing the secrets each developer must create locally
104105
├── .env.local # General local environment variables
105106
└── .gitignore
106107
107108
```
108109

109110
### Initial Migration & Future Migrations
110111

111-
For the new database path, the CLI creates Prisma files for managing migrations. Prisma is not required by AdminForth itself, but it is a convenient migration tool for standalone projects that do not have database management yet.
112+
When you answer **Yes** to `Include Prisma migrations? >`, the CLI creates Prisma files for managing migrations. Prisma is not required by AdminForth itself, but it is a convenient migration tool for standalone projects that do not have database management yet.
112113

113114
CLI will suggest you a command to initialize the database with Prisma:
114115

@@ -126,7 +127,7 @@ pnpm makemigration --name init ; pnpm migrate:local
126127

127128
Other developers need to pull migration and run `pnpm migrate:local` to apply any unapplied migrations.
128129

129-
For the existing database path, use your own migration tool instead. The generated project README shows how to add the required `adminuser` table to your database.
130+
When no Prisma migrations were generated — the database already had tables, you answered **No**, or the database is MongoDB or ClickHouse — use your own migration tool instead. The CLI output and the generated project README show how to add the required `adminuser` table to your database (MongoDB needs none).
130131

131132
## Run the Server
132133

‎adminforth/documentation/docs/tutorial/01-helloWorld.md‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,24 @@ Create two files in your project's root directory:
4747
Put the following content to the `.env.local` file:
4848

4949
```bash title="./.env.local"
50-
ADMINFORTH_SECRET=123
5150
NODE_ENV=development
5251
DATABASE_URL=sqlite://.db.sqlite
5352
PRISMA_DATABASE_URL=file:.db.sqlite
5453
```
5554

55+
Generate a signing key into the gitignored `.env` file, readable by you only (AdminForth logs a warning at startup if the secret is shorter than 16 characters; on Windows without openssl use `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`):
56+
57+
```bash
58+
(umask 077; echo "ADMINFORTH_SECRET=$(openssl rand -hex 32)" > .env)
59+
```
60+
61+
Make sure `.env` never reaches the repository or a Docker image:
62+
63+
```bash
64+
echo ".env" >> .gitignore
65+
echo ".env" >> .dockerignore
66+
```
67+
5668
> ☝️ Production best practices:
5769
>
5870
> 1) Most likely you not need `.env` file at all, instead you should use environment variables (from Docker, Kubernetes, Operating System, etc.)

‎adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ If your field has absolute URLs as text strings you can use `URLs` renderer to r
533533
534534
### Relative Time
535535
536-
To format your date fields to display the elapsed time, you can utilize the RelativeTime renderer.
536+
To format your date fields to display the elapsed time, you can utilize the RelativeTime renderer. Empty or invalid values render as an empty cell.
537537
538538
```ts title='./resources/anyResource.ts'
539539
columns: [

0 commit comments

Comments
 (0)