Ver Fonte

Added SQLite3 initialization

Efren Yevale Varela há 1 mês atrás
pai
commit
2a436e16b6
9 ficheiros alterados com 252 adições e 7 exclusões
  1. 3 0
      .gitignore
  2. 68 5
      AGENTS.md
  3. 24 0
      README.md
  4. 8 0
      config.json.example
  5. 1 0
      package.json
  6. 91 0
      pnpm-lock.yaml
  7. 1 0
      pnpm-workspace.yaml
  8. 13 0
      src/lib/crypto.js
  9. 43 2
      src/lib/sqlite3/database.js

+ 3 - 0
.gitignore

@@ -26,3 +26,6 @@ vite.config.ts.timestamp-*
 # Paraglide
 src/lib/paraglide
 project.inlang/cache/
+
+# SQLite3 Databases
+data

+ 68 - 5
AGENTS.md

@@ -31,11 +31,22 @@ npm run preview
 
 # Sync SvelteKit config (auto-runs on `npm install`)
 npm run prepare
+
+# Start production server (requires `npm run build` first)
+npm start
 ```
 
 ### No test/lint commands
 This is a fresh project with no test suite or linter configured. Do not assume these exist.
 
+### Setup Prerequisites
+
+Before first dev/build run, **MUST copy configuration**:
+```bash
+cp config.json.example config.json
+```
+Database is auto-initialized on first run (creates `/data/app.db`, seeds admin user `admin@delete.me` / `deleteme`).
+
 ## Architecture & Key Files
 
 ### SvelteKit Project Structure
@@ -44,12 +55,18 @@ This is a fresh project with no test suite or linter configured. Do not assume t
 src/
 ├── app.html           # HTML shell (dark mode preset)
 ├── hooks.js           # Paraglide i18n route localization
+├── hooks.server.js    # Graceful shutdown, db cleanup, signal handlers
 ├── routes/
 │   ├── +layout.svelte # Root layout component
 │   ├── +page.svelte   # Home page (Flowbite Alert, language switcher, dark mode toggle)
+│   ├── +error.svelte  # Error page
 │   └── layout.css     # Route-level styles
 └── lib/
-    ├── index.js       # $lib alias exports (currently empty)
+    ├── index.js       # $lib alias exports
+    ├── config.js      # Loads config.json (db & password settings)
+    ├── crypto.js      # Argon2id hashing & verification (hashPassword, verifyPassword)
+    ├── sqlite3/
+    │   └── database.js # Database initialization, schema, admin user seeding
     ├── paraglide/     # i18n (auto-generated, DO NOT EDIT)
     │   ├── messages.js          # Locale-aware message functions
     │   ├── runtime.js           # getLocale(), setLocale(), deLocalizeUrl()
@@ -57,6 +74,14 @@ src/
     └── assets/        # Static assets
 ```
 
+### Backend Infrastructure
+
+- **Configuration**: `config.json` (loaded by `src/lib/config.js`, must exist before runtime)
+- **Database**: SQLite3 (auto-initialized in `/data/app.db` on first run)
+- **Schema**: `users` table with `id`, `email`, `password` (hashed), `created_at`, `updated_at`
+- **Auth**: Argon2id password hashing via `src/lib/crypto.js`
+- **Admin User**: Auto-seeded if users table is empty (`admin@delete.me` / `deleteme`)
+
 ### Build Artifacts (gitignored)
 
 - `.svelte-kit/` — SvelteKit type hints & config cache
@@ -103,10 +128,10 @@ runes: ({ filename }) =>
 ## Git & Workflow
 
 - **Branch**: master
-- **Recent commits**: 5 commits total (initial + dark mode + i18n + documentation + icon fix)
-- **Status**: Clean working tree (no staged/unstaged changes)
+- **Recent commits**: 14 commits total (backend infrastructure: SQLite3, Argon2id, config management, graceful shutdown)
+- **Current state**: 6 files staged for commit (crypto, database setup, config changes)
 
-**Convention**: No established commit message convention yet. Keep it clear and atomic.
+**Convention**: Clear, atomic commits. Prefix context: "feat:", "fix:", "docs:", "refactor:" when possible.
 
 ## Known Issues & Observations
 
@@ -116,6 +141,8 @@ runes: ({ filename }) =>
 
 3. **Minimal jsconfig**: Inherits TypeScript config from `.svelte-kit/tsconfig.json` but doesn't enforce type checking (`checkJs: false`)
 
+4. **Config must exist**: Application crashes at startup if `config.json` is missing. Always ensure it's copied from `config.json.example` before running dev/build.
+
 ## Common Agent Tasks & Gotchas
 
 ### Adding Components
@@ -142,6 +169,42 @@ runes: ({ filename }) =>
 - Reference via `import { env } from '$env/dynamic/public'` or `$env/static/public`
 - Prefix public vars with `VITE_` for client access
 
+## Database & Configuration
+
+### Configuration Loading (`src/lib/config.js`)
+
+The application loads `config.json` at startup. This must exist before running dev/build:
+
+```bash
+cp config.json.example config.json
+```
+
+**Failure mode**: If missing, the app crashes with `Failed to load config.json` error during `npm run dev` or `npm run build`.
+
+### Database Initialization (`src/lib/sqlite3/database.js`)
+
+- **Lazy initialization**: Database is created on first `getSqlite3()` call
+- **Schema**: `users` table auto-created with `id`, `email`, `password`, `created_at`, `updated_at`
+- **Admin seeding**: If no users exist, `admin@delete.me` / `deleteme` is inserted (hashed with Argon2id)
+- **Path**: `/data/app.db` (relative to project root, directory auto-created if missing)
+
+**Idempotent**: Safe to call multiple times; schema and admin user only created once.
+
+### Password Hashing (`src/lib/crypto.js`)
+
+Two exported functions:
+- `hashPassword(password)` — async, returns Argon2id hash (configured in `config.json`)
+- `verifyPassword(password, hash)` — async, returns boolean
+
+Use these for any user auth operations. Config pulls settings from `config.password.options`.
+
+### Graceful Shutdown (`src/hooks.server.js`)
+
+Database connection is properly closed on process termination (SIGTERM, SIGINT) or runtime errors. Exit codes:
+- `0` — Graceful shutdown (SIGTERM/SIGINT)
+- `1` — Uncaught exception or unhandled rejection
+- `2` — Cleanup error during shutdown
+
 ## Performance Notes
 
 - Vite dev server is fast; rebuilds are near-instant
@@ -157,4 +220,4 @@ runes: ({ filename }) =>
 
 ---
 
-**Last Updated**: Verified against current project state (5 commits, i18n enabled, no typos, clean build)
+**Last Updated**: Verified against current project state (14 commits, backend infrastructure added, i18n enabled, clean build)

+ 24 - 0
README.md

@@ -113,6 +113,14 @@ The `config.json` file (copied from `config.json.example`) controls server and d
         "tempStore": "memory"
       }
     }
+  },
+  "password": {
+    "algorithm": "argon2id",
+    "options": {
+      "memoryCost": 61440,
+      "timeCost": 3,
+      "parallelism": 1
+    }
   }
 }
 ```
@@ -121,6 +129,20 @@ The `config.json` file (copied from `config.json.example`) controls server and d
 - **`database.sqlite3.filename`** — Path to the SQLite database file
 - **`database.sqlite3.timeout`** — Query timeout in milliseconds
 - **`database.sqlite3.performance`** — Performance tuning options (WAL mode, foreign keys, etc.)
+- **`password.algorithm`** — Password hashing algorithm (currently `argon2id`)
+- **`password.options`** — Argon2id configuration (memory cost in KiB, time cost iterations, parallelism level)
+
+### Database Initialization
+
+The database is automatically initialized on first creation with:
+- **`users` table** — Stores user accounts with `email` and `password` (hashed) fields
+- **Admin user** — Created automatically with email `admin@delete.me` and password `deleteme` (hashed with argon2id)
+
+Database initialization is idempotent and runs only once when the database file is first created. The admin user is seeded only if no users exist.
+
+### Password Security
+
+Passwords are hashed using **Argon2id** (configured in `config.json`). Crypto utilities are available in `src/lib/crypto.js` for hashing and verification.
 
 ## Exit Codes
 
@@ -140,4 +162,6 @@ Exit codes are defined in `src/hooks.server.js` and are emitted when the process
 - [Flowbite Svelte](https://flowbite-svelte.com/)
 - [Tailwind CSS](https://tailwindcss.com/)
 - [Paraglide JS](https://paraglidejs.com/vite)
+- [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)
+- [node-argon2](https://github.com/ranisalt/node-argon2)
 - [Project Instructions](./AGENTS.md)

+ 8 - 0
config.json.example

@@ -13,5 +13,13 @@
         "tempStore": "memory"
       }
     }
+  },
+  "password": {
+    "algorithm": "argon2id",
+    "options": {
+      "memoryCost": 61440,
+      "timeCost": 3,
+      "parallelism": 1
+    }
   }
 }

+ 1 - 0
package.json

@@ -26,6 +26,7 @@
 		"vite": "^8.1.0"
 	},
 	"dependencies": {
+		"argon2": "^0.44.0",
 		"better-sqlite3": "^12.11.1"
 	}
 }

+ 91 - 0
pnpm-lock.yaml

@@ -8,6 +8,9 @@ importers:
 
   .:
     dependencies:
+      argon2:
+        specifier: ^0.44.0
+        version: 0.44.0
       better-sqlite3:
         specifier: ^12.11.1
         version: 12.11.1
@@ -67,6 +70,9 @@ packages:
   '@emnapi/wasi-threads@1.2.2':
     resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
 
+  '@epic-web/invariant@1.0.0':
+    resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
+
   '@floating-ui/core@1.7.5':
     resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
 
@@ -124,6 +130,10 @@ packages:
   '@oxc-project/types@0.137.0':
     resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==}
 
+  '@phc/format@1.0.0':
+    resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
+    engines: {node: '>=10'}
+
   '@polka/url@1.0.0-next.29':
     resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
 
@@ -593,6 +603,10 @@ packages:
   apexcharts@5.15.2:
     resolution: {integrity: sha512-qbd+ehDiRxo++wAqaGLmJcd683ktulTqoku4WYYyQ3XOgJIn4yhOQGV27KcdK7c2yhwxblyh4mgMm8ukI0wC0Q==}
 
+  argon2@0.44.0:
+    resolution: {integrity: sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==}
+    engines: {node: '>=16.17.0'}
+
   aria-query@5.3.1:
     resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
     engines: {node: '>= 0.4'}
@@ -646,6 +660,15 @@ packages:
     resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
     engines: {node: '>= 0.6'}
 
+  cross-env@10.1.0:
+    resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
+    engines: {node: '>=20'}
+    hasBin: true
+
+  cross-spawn@7.0.6:
+    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+    engines: {node: '>= 8'}
+
   cssesc@3.0.0:
     resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
     engines: {node: '>=4'}
@@ -800,6 +823,9 @@ packages:
   is-reference@3.0.3:
     resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
 
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
   jiti@2.7.0:
     resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
     hasBin: true
@@ -930,6 +956,14 @@ packages:
     resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==}
     engines: {node: '>=10'}
 
+  node-addon-api@8.9.0:
+    resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==}
+    engines: {node: ^18 || ^20 || >= 21}
+
+  node-gyp-build@4.8.4:
+    resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
+    hasBin: true
+
   obug@2.1.3:
     resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
     engines: {node: '>=12.20.0'}
@@ -937,6 +971,10 @@ packages:
   once@1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
 
+  path-key@3.1.1:
+    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+    engines: {node: '>=8'}
+
   path-parse@1.0.7:
     resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
 
@@ -998,6 +1036,14 @@ packages:
   set-cookie-parser@3.1.1:
     resolution: {integrity: sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==}
 
+  shebang-command@2.0.0:
+    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+    engines: {node: '>=8'}
+
+  shebang-regex@3.0.0:
+    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+    engines: {node: '>=8'}
+
   simple-concat@1.0.1:
     resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
 
@@ -1141,6 +1187,11 @@ packages:
   webpack-virtual-modules@0.6.2:
     resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
 
+  which@2.0.2:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+    engines: {node: '>= 8'}
+    hasBin: true
+
   wrappy@1.0.2:
     resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
 
@@ -1167,6 +1218,8 @@ snapshots:
       tslib: 2.8.1
     optional: true
 
+  '@epic-web/invariant@1.0.0': {}
+
   '@floating-ui/core@1.7.5':
     dependencies:
       '@floating-ui/utils': 0.2.11
@@ -1246,6 +1299,8 @@ snapshots:
 
   '@oxc-project/types@0.137.0': {}
 
+  '@phc/format@1.0.0': {}
+
   '@polka/url@1.0.0-next.29': {}
 
   '@popperjs/core@2.11.8': {}
@@ -1578,6 +1633,13 @@ snapshots:
 
   apexcharts@5.15.2: {}
 
+  argon2@0.44.0:
+    dependencies:
+      '@phc/format': 1.0.0
+      cross-env: 10.1.0
+      node-addon-api: 8.9.0
+      node-gyp-build: 4.8.4
+
   aria-query@5.3.1: {}
 
   array-timsort@1.0.3: {}
@@ -1623,6 +1685,17 @@ snapshots:
 
   cookie@0.6.0: {}
 
+  cross-env@10.1.0:
+    dependencies:
+      '@epic-web/invariant': 1.0.0
+      cross-spawn: 7.0.6
+
+  cross-spawn@7.0.6:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
   cssesc@3.0.0: {}
 
   date-fns@4.4.0: {}
@@ -1770,6 +1843,8 @@ snapshots:
     dependencies:
       '@types/estree': 1.0.9
 
+  isexe@2.0.0: {}
+
   jiti@2.7.0: {}
 
   js-sha256@0.11.1: {}
@@ -1853,12 +1928,18 @@ snapshots:
     dependencies:
       semver: 7.8.5
 
+  node-addon-api@8.9.0: {}
+
+  node-gyp-build@4.8.4: {}
+
   obug@2.1.3: {}
 
   once@1.4.0:
     dependencies:
       wrappy: 1.0.2
 
+  path-key@3.1.1: {}
+
   path-parse@1.0.7: {}
 
   picocolors@1.1.1: {}
@@ -1974,6 +2055,12 @@ snapshots:
 
   set-cookie-parser@3.1.1: {}
 
+  shebang-command@2.0.0:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  shebang-regex@3.0.0: {}
+
   simple-concat@1.0.1: {}
 
   simple-get@4.0.1:
@@ -2095,6 +2182,10 @@ snapshots:
 
   webpack-virtual-modules@0.6.2: {}
 
+  which@2.0.2:
+    dependencies:
+      isexe: 2.0.0
+
   wrappy@1.0.2: {}
 
   zimmerframe@1.1.4: {}

+ 1 - 0
pnpm-workspace.yaml

@@ -2,3 +2,4 @@ allowBuilds:
   esbuild: true
   '@tailwindcss/oxide': true
   better-sqlite3: true
+  argon2: false

+ 13 - 0
src/lib/crypto.js

@@ -0,0 +1,13 @@
+import config from './config.js';
+
+import { hash, verify } from 'argon2';
+
+const passwordConfig = config.password;
+
+export async function hashPassword(password) {
+  return hash(password, passwordConfig.options);
+}
+
+export async function verifyPassword(password, hash) {
+  return verify(hash, password);
+}

+ 43 - 2
src/lib/sqlite3/database.js

@@ -1,18 +1,54 @@
 import Database from 'better-sqlite3';
 import config   from '../config.js';
 import path     from 'path';
+import fs       from 'fs';
 
 import { fileURLToPath } from 'url';
+import { hashPassword } from '../crypto.js';
 
 const __dirname    = path.dirname(fileURLToPath(import.meta.url));
-const PROJECT_ROOT = path.resolve(__dirname, '../../');
+const PROJECT_ROOT = path.resolve(__dirname, '../../../');
 const DB_PATH      = path.join(PROJECT_ROOT, 'data', config.database.sqlite3.filename);
+const DB_DIR       = path.dirname(DB_PATH);
 
 let db = null;
 
-export function getSqlite3() {
+async function initializeSchema() {
+  // Create users table
+  db.exec(`
+    CREATE TABLE IF NOT EXISTS users (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      email TEXT UNIQUE NOT NULL,
+      password TEXT NOT NULL,
+      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+    );
+  `);
+}
+
+async function seedAdminUser() {
+  const stmt = db.prepare('SELECT COUNT(*) as count FROM users');
+  const result = stmt.get();
+
+  // Only seed if no users exist
+  if (result.count === 0) {
+    const hashedPassword = await hashPassword('deleteme');
+    const insertStmt = db.prepare(
+      'INSERT INTO users (email, password) VALUES (?, ?)'
+    );
+    insertStmt.run('admin@delete.me', hashedPassword);
+  }
+}
+
+export async function getSqlite3() {
   if (db) return db;
 
+  if (!fs.existsSync(DB_DIR)) {
+    fs.mkdirSync(DB_DIR, { recursive: true });
+  }
+
+  const isNewDb = !fs.existsSync(DB_PATH);
+
   const dbConfig = config.database.sqlite3;
   db = new Database(DB_PATH, {
     fileMustExist: dbConfig.fileMustExist,
@@ -25,6 +61,11 @@ export function getSqlite3() {
   db.pragma(`synchronous = ${dbConfig.performance.synchronous}`);
   db.pragma(`temp_store = ${dbConfig.performance.tempStore}`);
 
+  if (isNewDb) {
+    await initializeSchema();
+    await seedAdminUser();
+  }
+
   return db;
 }