@mostajs/orm-cli 0.3.0 → 0.3.2

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.
Files changed (2) hide show
  1. package/bin/mostajs.sh +256 -5
  2. package/package.json +2 -1
package/bin/mostajs.sh CHANGED
@@ -548,6 +548,7 @@ menu_databases() {
548
548
  echo -e " ${CYAN}p${RESET}) APP_PORT : ${DIM}${APP_PORT:-3000}${RESET}"
549
549
  echo
550
550
  echo -e " ${CYAN}t${RESET}) Test all connections"
551
+ echo -e " ${CYAN}i${RESET}) ${BOLD}Import from project .env${RESET} (auto-detect app's real DB)"
551
552
  echo -e " ${CYAN}e${RESET}) Export to .env.local in project"
552
553
  echo -e " ${CYAN}r${RESET}) Reset config"
553
554
  echo -e " ${CYAN}b${RESET}) Back"
@@ -565,6 +566,7 @@ menu_databases() {
565
566
  n|N) save_var MOSTA_NET_TRANSPORT "$(ask 'MOSTA_NET_TRANSPORT (rest|sse|graphql|mcp|websocket|jsonrpc|grpc|odata)' "${MOSTA_NET_TRANSPORT:-rest}")";;
566
567
  p|P) save_var APP_PORT "$(ask 'APP_PORT' "${APP_PORT:-3000}")";;
567
568
  t|T) action_test_connections; return;;
569
+ i|I) action_import_env ;;
568
570
  e|E) export_env_local ;;
569
571
  r|R) confirm "Really reset config?" && rm -f "$CONFIG_FILE" && ok "Reset";;
570
572
  b|B) return;;
@@ -647,6 +649,95 @@ list_extra_bindings() {
647
649
  done
648
650
  }
649
651
 
652
+ # Import DB config from the project's own .env file.
653
+ # This is CRUCIAL when your app (Prisma, NextAuth, ...) already has a
654
+ # DATABASE_URL / MONGODB_URI / POSTGRES_URL. Seeding the wrong DB leads to
655
+ # the classic "users are in mosta-net /api/v1/users but login fails" bug.
656
+ action_import_env() {
657
+ header
658
+ echo -e "${BOLD}${MAGENTA}▶ Import DB config from project .env${RESET}"
659
+ echo
660
+
661
+ local candidates=(".env.local" ".env" ".env.production" ".env.development")
662
+ local -a available=()
663
+ for f in "${candidates[@]}"; do
664
+ [[ -f "$PROJECT_ROOT/$f" ]] && available+=("$f")
665
+ done
666
+
667
+ if [[ ${#available[@]} -eq 0 ]]; then
668
+ err "No .env* file found in $PROJECT_ROOT"
669
+ pause; return
670
+ fi
671
+
672
+ echo "Found env files :"
673
+ local i=1
674
+ for f in "${available[@]}"; do
675
+ echo -e " ${CYAN}$i${RESET}) $f"
676
+ i=$((i+1))
677
+ done
678
+ echo
679
+ local choice; choice=$(ask "Number" 1)
680
+ local src_file="${available[$((choice-1))]}"
681
+ [[ -z "$src_file" ]] && return
682
+ local src_path="$PROJECT_ROOT/$src_file"
683
+
684
+ # Extract common DB URI variables — priority order matters
685
+ # MONGODB_URI > DATABASE_URL > POSTGRES_URL > MYSQL_URL > SGBD_URI
686
+ local found_uri="" found_var="" found_dialect=""
687
+ for var in SGBD_URI DATABASE_URL MONGODB_URI POSTGRES_URL POSTGRES_URI MYSQL_URL MYSQL_URI POSTGRESQL_URL; do
688
+ # Read value, skip commented lines, strip quotes
689
+ local val
690
+ val=$(grep -E "^${var}=" "$src_path" 2>/dev/null | grep -v "^#" | head -1 | sed "s/^${var}=//" | sed 's/^"\(.*\)"$/\1/' | sed "s/^'\(.*\)'$/\1/")
691
+ if [[ -n "$val" ]]; then
692
+ found_uri="$val"
693
+ found_var="$var"
694
+ break
695
+ fi
696
+ done
697
+
698
+ if [[ -z "$found_uri" ]]; then
699
+ err "No DB URI found in $src_file"
700
+ info "Searched : SGBD_URI, DATABASE_URL, MONGODB_URI, POSTGRES_URL/URI, MYSQL_URL/URI"
701
+ pause; return
702
+ fi
703
+
704
+ # Detect dialect from URI scheme
705
+ found_dialect=$(detect_dialect_from_uri "$found_uri")
706
+ if [[ "$found_dialect" == "unknown" ]]; then
707
+ warn "Could not auto-detect dialect from URI: $found_uri"
708
+ found_dialect=$(ask "Dialect name (mongodb|postgres|mysql|...)" "")
709
+ [[ -z "$found_dialect" ]] && { pause; return; }
710
+ fi
711
+
712
+ info "Detected :"
713
+ echo -e " Variable : ${CYAN}$found_var${RESET}"
714
+ echo -e " URI : ${DIM}$found_uri${RESET}"
715
+ echo -e " Dialect : ${MAGENTA}$found_dialect${RESET}"
716
+ echo
717
+
718
+ # Special Prisma note
719
+ if [[ "$found_var" == "MONGODB_URI" ]] || [[ "$found_var" == "DATABASE_URL" ]]; then
720
+ dim " (This is likely the Prisma datasource — the same DB your app uses for login/auth.)"
721
+ echo
722
+ fi
723
+
724
+ if confirm "Save as DB_DIALECT + SGBD_URI ?"; then
725
+ save_var DB_DIALECT "$found_dialect"
726
+ save_var SGBD_URI "$found_uri"
727
+ # Suggest a strategy based on environment context
728
+ if [[ "$src_file" == ".env.production" ]]; then
729
+ save_var DB_SCHEMA_STRATEGY "validate"
730
+ info "Auto-set DB_SCHEMA_STRATEGY=validate (production)"
731
+ elif [[ -z "${DB_SCHEMA_STRATEGY:-}" ]]; then
732
+ save_var DB_SCHEMA_STRATEGY "update"
733
+ fi
734
+ ok "Config imported"
735
+ echo
736
+ info "Next step : menu 2 → t (test connection), then menu S → 4 (seed)"
737
+ fi
738
+ pause
739
+ }
740
+
650
741
  # Export a .env.local file compatible with @mostajs/orm convention
651
742
  export_env_local() {
652
743
  local target="$PROJECT_ROOT/.env.mostajs"
@@ -1508,6 +1599,7 @@ menu_seeding() {
1508
1599
  echo -e " ${CYAN}7${RESET}) Dump current DB rows → .mostajs/seeds-dump/"
1509
1600
  echo -e " ${CYAN}8${RESET}) Clear the seeds directory"
1510
1601
  echo -e " ${CYAN}9${RESET}) Show a seed file"
1602
+ echo -e " ${CYAN}h${RESET}) ${BOLD}Hash plain-text passwords in seed files${RESET} (bcrypt)"
1511
1603
  echo
1512
1604
  echo -e " ${CYAN}b${RESET}) Back"
1513
1605
  echo
@@ -1523,12 +1615,105 @@ menu_seeding() {
1523
1615
  7) action_seed_dump ;;
1524
1616
  8) action_seed_clear ;;
1525
1617
  9) action_seed_show ;;
1618
+ h|H) action_seed_hash_passwords ;;
1526
1619
  b|B) return ;;
1527
1620
  *) warn "Unknown"; pause ;;
1528
1621
  esac
1529
1622
  menu_seeding
1530
1623
  }
1531
1624
 
1625
+ # ------------------------------------------------------------
1626
+ # seed : hash plain-text passwords
1627
+ # ------------------------------------------------------------
1628
+
1629
+ action_seed_hash_passwords() {
1630
+ header
1631
+ echo -e "${BOLD}${MAGENTA}▶ Hash plain-text passwords in seed files${RESET}"
1632
+ echo
1633
+
1634
+ local seed_dir="$CONFIG_DIR/seeds"
1635
+ local count
1636
+ count=$(ls -1 "$seed_dir"/*.json 2>/dev/null | wc -l)
1637
+ if [[ $count -eq 0 ]]; then
1638
+ err "No seed files in $seed_dir (menu S → 1 first)"
1639
+ pause; return
1640
+ fi
1641
+
1642
+ echo "Auto-detects fields named :"
1643
+ dim " password, passwordHash, hashedPassword, pwd, userPassword"
1644
+ echo
1645
+ echo "Skips values that are already bcrypt hashes (start with \$2a\$ / \$2b\$ / \$2y\$, 60 chars)"
1646
+ echo
1647
+
1648
+ local cost
1649
+ cost=$(ask "bcrypt cost factor" "${BCRYPT_COST:-10}")
1650
+ if ! confirm "Hash all plain passwords in $seed_dir ?"; then
1651
+ return
1652
+ fi
1653
+
1654
+ # Ensure bcryptjs is available
1655
+ ensure_pkg "bcryptjs" || { pause; return; }
1656
+
1657
+ cat > "$CONFIG_DIR/seed-hash.mjs" <<EOF
1658
+ import { readdirSync, readFileSync, writeFileSync } from 'fs';
1659
+ import { join } from 'path';
1660
+ import bcrypt from 'bcryptjs';
1661
+
1662
+ const SEEDDIR = '$seed_dir';
1663
+ const COST = $cost;
1664
+
1665
+ // Field names commonly used for passwords
1666
+ const PASSWORD_FIELDS = ['password', 'passwordHash', 'hashedPassword', 'pwd', 'userPassword'];
1667
+
1668
+ // Test if a string is already a bcrypt hash
1669
+ const isBcrypt = v => typeof v === 'string' && /^\\\$2[ayb]\\\$\\d{1,2}\\\$/.test(v) && v.length === 60;
1670
+
1671
+ let totalHashed = 0;
1672
+ let totalSkipped = 0;
1673
+ const summary = [];
1674
+
1675
+ for (const f of readdirSync(SEEDDIR).filter(x => x.endsWith('.json'))) {
1676
+ const path = join(SEEDDIR, f);
1677
+ let data;
1678
+ try { data = JSON.parse(readFileSync(path, 'utf8')); }
1679
+ catch { continue; }
1680
+
1681
+ if (!Array.isArray(data)) continue;
1682
+
1683
+ let hashed = 0, skipped = 0;
1684
+ for (const row of data) {
1685
+ for (const field of PASSWORD_FIELDS) {
1686
+ const val = row[field];
1687
+ if (typeof val !== 'string' || val.length === 0) continue;
1688
+ if (isBcrypt(val)) { skipped++; continue; }
1689
+ row[field] = bcrypt.hashSync(val, COST);
1690
+ hashed++;
1691
+ }
1692
+ }
1693
+
1694
+ if (hashed > 0) {
1695
+ writeFileSync(path, JSON.stringify(data, null, 2));
1696
+ summary.push({ file: f, hashed, skipped });
1697
+ }
1698
+ totalHashed += hashed;
1699
+ totalSkipped += skipped;
1700
+ }
1701
+
1702
+ for (const s of summary) {
1703
+ console.log(' \u2713 ' + s.file + ' : ' + s.hashed + ' hashed' + (s.skipped ? ' (+ ' + s.skipped + ' already hashed)' : ''));
1704
+ }
1705
+ console.log();
1706
+ console.log('Total : ' + totalHashed + ' hashed, ' + totalSkipped + ' already-hashed skipped');
1707
+ if (totalHashed === 0 && totalSkipped === 0) {
1708
+ console.log('No password fields found.');
1709
+ }
1710
+ EOF
1711
+ cd "$PROJECT_ROOT"
1712
+ node "$CONFIG_DIR/seed-hash.mjs" 2>&1 | tee "$LOG_DIR/seed-hash.log"
1713
+ echo
1714
+ pause
1715
+ }
1716
+
1532
1717
  # ------------------------------------------------------------
1533
1718
  # seed : upload / import
1534
1719
  # ------------------------------------------------------------
@@ -1750,6 +1935,9 @@ for (const f of readdirSync(SEEDDIR).filter(x => x.endsWith('.json'))) {
1750
1935
  }
1751
1936
 
1752
1937
  // ---------- Validate ----------
1938
+ const PASSWORD_FIELDS = ['password', 'passwordHash', 'hashedPassword', 'pwd', 'userPassword'];
1939
+ const isBcrypt = v => typeof v === 'string' && /^\\\$2[ayb]\\\$\\d{1,2}\\\$/.test(v) && v.length === 60;
1940
+
1753
1941
  function validateRow(row, entity) {
1754
1942
  const errors = [];
1755
1943
  const fieldNames = new Set(Object.keys(entity.fields ?? {}));
@@ -1780,6 +1968,12 @@ function validateRow(row, entity) {
1780
1968
  warnings.push('unknown field "' + k + '" (not in schema)');
1781
1969
  }
1782
1970
  }
1971
+ // Password fields that look like plain-text (not bcrypt)
1972
+ for (const pf of PASSWORD_FIELDS) {
1973
+ if (row[pf] !== undefined && row[pf] !== null && row[pf] !== '' && !isBcrypt(row[pf])) {
1974
+ warnings.push('field "' + pf + '" does not look like a bcrypt hash — run menu S \u2192 h to hash');
1975
+ }
1976
+ }
1783
1977
  return { errors, warnings };
1784
1978
  }
1785
1979
 
@@ -2122,6 +2316,55 @@ EOF
2122
2316
 
2123
2317
  run_subcommand() {
2124
2318
  case "$1" in
2319
+ hash|h)
2320
+ # mostajs hash <plaintext> [cost]
2321
+ local pw="${2:-}"
2322
+ local cost="${3:-10}"
2323
+ [[ -z "$pw" ]] && { echo "Usage: mostajs hash <password> [cost=10]" >&2; exit 1; }
2324
+ # Try local project, then CLI's own node_modules (bcryptjs is a dep)
2325
+ local bcrypt_dir=""
2326
+ if [[ -d "$PROJECT_ROOT/node_modules/bcryptjs" ]]; then
2327
+ bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
2328
+ else
2329
+ local cli_dir
2330
+ cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
2331
+ [[ -d "$cli_dir/node_modules/bcryptjs" ]] && bcrypt_dir="$cli_dir/node_modules/bcryptjs"
2332
+ fi
2333
+ if [[ -z "$bcrypt_dir" ]]; then
2334
+ ensure_pkg "bcryptjs" >/dev/null 2>&1 || {
2335
+ err "bcryptjs not available. Install it manually : npm install bcryptjs"
2336
+ exit 1
2337
+ }
2338
+ bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
2339
+ fi
2340
+ BCRYPT_PASSWORD="$pw" BCRYPT_COST="$cost" BCRYPT_DIR="$bcrypt_dir" node -e "
2341
+ const bcrypt = require(process.env.BCRYPT_DIR);
2342
+ const h = bcrypt.hashSync(process.env.BCRYPT_PASSWORD, parseInt(process.env.BCRYPT_COST, 10));
2343
+ console.log(h);
2344
+ "
2345
+ ;;
2346
+ verify|v)
2347
+ # mostajs verify <plaintext> <hash>
2348
+ local pw="${2:-}"
2349
+ local hashval="${3:-}"
2350
+ [[ -z "$pw" || -z "$hashval" ]] && { echo "Usage: mostajs verify <password> <hash>" >&2; exit 1; }
2351
+ local bcrypt_dir=""
2352
+ if [[ -d "$PROJECT_ROOT/node_modules/bcryptjs" ]]; then
2353
+ bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
2354
+ else
2355
+ local cli_dir
2356
+ cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
2357
+ [[ -d "$cli_dir/node_modules/bcryptjs" ]] && bcrypt_dir="$cli_dir/node_modules/bcryptjs"
2358
+ fi
2359
+ if [[ -z "$bcrypt_dir" ]]; then
2360
+ ensure_pkg "bcryptjs" >/dev/null 2>&1 || exit 1
2361
+ bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
2362
+ fi
2363
+ BCRYPT_PASSWORD="$pw" BCRYPT_HASH="$hashval" BCRYPT_DIR="$bcrypt_dir" node -e "
2364
+ const bcrypt = require(process.env.BCRYPT_DIR);
2365
+ console.log(bcrypt.compareSync(process.env.BCRYPT_PASSWORD, process.env.BCRYPT_HASH) ? 'match' : 'no match');
2366
+ "
2367
+ ;;
2125
2368
  convert|c)
2126
2369
  detect_project
2127
2370
  [[ ${#DETECTED_TYPES[@]} -eq 0 ]] && { err "No schema found"; exit 1; }
@@ -2150,11 +2393,17 @@ run_subcommand() {
2150
2393
  help|-h|--help)
2151
2394
  cat <<EOF
2152
2395
  Usage :
2153
- $CLI_NAME Interactive menu
2154
- $CLI_NAME convert Run conversion (auto-detect schema type)
2155
- $CLI_NAME detect Print detected schemas
2156
- $CLI_NAME health Run health checks
2157
- $CLI_NAME version Print version
2396
+ $CLI_NAME Interactive menu
2397
+ $CLI_NAME convert Run conversion (auto-detect schema type)
2398
+ $CLI_NAME detect Print detected schemas
2399
+ $CLI_NAME health Run health checks
2400
+ $CLI_NAME hash <password> [cost] Hash a password with bcrypt (cost default 10)
2401
+ $CLI_NAME verify <password> <hash> Check if a plain password matches a bcrypt hash
2402
+ $CLI_NAME version Print version
2403
+
2404
+ Examples:
2405
+ $CLI_NAME hash 'Admin@123456' → \$2b\$10\$N9qo8uLOickgx2ZMRZoMyeIjZA...
2406
+ $CLI_NAME verify 'Admin@123456' '\$2b\$10\$N9qo...'
2158
2407
  EOF
2159
2408
  ;;
2160
2409
  *)
@@ -2175,6 +2424,8 @@ EOF
2175
2424
 
2176
2425
  # Non-interactive mode if args provided
2177
2426
  if [[ $# -gt 0 ]]; then
2427
+ detect_project # populates PKG_MANAGER, PROJECT_ROOT, etc.
2428
+ load_env # optional config for subcommands that need it
2178
2429
  run_subcommand "$@"
2179
2430
  exit 0
2180
2431
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Universal CLI to integrate @mostajs/orm into any project — auto-detects Prisma, OpenAPI, JSON Schema. Interactive menu + subcommands. 13 databases.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
@@ -43,6 +43,7 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@mostajs/orm": "^1.9.2",
46
+ "bcryptjs": "^2.4.3",
46
47
  "better-sqlite3": "^12.9.0"
47
48
  }
48
49
  }