kreftjs 0.0.0-stage → 0.0.2-beta

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 (184) hide show
  1. package/README.md +103 -2
  2. package/bin/add.js +1074 -0
  3. package/bin/artisan-more.js +994 -0
  4. package/bin/artisan.js +1877 -0
  5. package/bin/backend/app.js +161 -0
  6. package/bin/backend/cache.js +196 -0
  7. package/bin/backend/collection.js +117 -0
  8. package/bin/backend/command.js +45 -0
  9. package/bin/backend/config.js +234 -0
  10. package/bin/backend/error-page.js +773 -0
  11. package/bin/backend/events.js +49 -0
  12. package/bin/backend/factory.js +223 -0
  13. package/bin/backend/fake.js +180 -0
  14. package/bin/backend/form.js +102 -0
  15. package/bin/backend/gate.js +79 -0
  16. package/bin/backend/gc.js +69 -0
  17. package/bin/backend/html.js +322 -0
  18. package/bin/backend/http.js +652 -0
  19. package/bin/backend/index.js +452 -0
  20. package/bin/backend/mail.js +77 -0
  21. package/bin/backend/mcp.js +433 -0
  22. package/bin/backend/migrator.js +370 -0
  23. package/bin/backend/model.js +781 -0
  24. package/bin/backend/namespace.js +213 -0
  25. package/bin/backend/notifications.js +68 -0
  26. package/bin/backend/qr.js +164 -0
  27. package/bin/backend/query.js +387 -0
  28. package/bin/backend/queue.js +412 -0
  29. package/bin/backend/redis.js +479 -0
  30. package/bin/backend/schedule.js +112 -0
  31. package/bin/backend/schema.js +321 -0
  32. package/bin/backend/seo-pages.js +100 -0
  33. package/bin/backend/storage.js +153 -0
  34. package/bin/backend/support.js +567 -0
  35. package/bin/backend/typescript.js +166 -0
  36. package/bin/build.js +540 -0
  37. package/bin/db-api.js +249 -0
  38. package/bin/db.js +275 -0
  39. package/bin/deps.js +138 -0
  40. package/bin/electron.js +509 -0
  41. package/bin/env.js +97 -0
  42. package/bin/guard.js +80 -0
  43. package/bin/kreftjs-config.js +40 -0
  44. package/bin/kreftjs.js +1373 -0
  45. package/bin/lang.js +241 -0
  46. package/bin/layout.js +74 -0
  47. package/bin/no-auth.js +162 -0
  48. package/bin/prerender.js +258 -0
  49. package/bin/safe-json.js +24 -0
  50. package/bin/server-routes.js +154 -0
  51. package/bin/ssr.js +133 -0
  52. package/bin/terminal.js +195 -0
  53. package/bin/to-ts.js +36 -0
  54. package/bin/update.js +237 -0
  55. package/bin/urls-file.js +27 -0
  56. package/bin/vite.js +228 -0
  57. package/docs/Docs.kreftjs +557 -0
  58. package/docs/README.md +2302 -0
  59. package/docs/markdown.js +186 -0
  60. package/editors/vscode/README.md +13 -0
  61. package/editors/vscode/extension.js +183 -0
  62. package/editors/vscode/language-configuration.json +17 -0
  63. package/editors/vscode/package.json +57 -0
  64. package/editors/vscode/snippets/kreftjs.json +21 -0
  65. package/editors/vscode/syntaxes/kreftjs.tmLanguage.json +207 -0
  66. package/kreftjs.d.ts +579 -0
  67. package/kreftjs.js +5108 -0
  68. package/package.json +97 -4
  69. package/server.d.ts +1028 -0
  70. package/server.js +8 -0
  71. package/templates/app/README.md +127 -0
  72. package/templates/app/_env.example +64 -0
  73. package/templates/app/_gitignore +5 -0
  74. package/templates/app/_vscode/settings.json +9 -0
  75. package/templates/app/backend/app/Auth/Session.js +177 -0
  76. package/templates/app/backend/app/Auth/Throttle.js +29 -0
  77. package/templates/app/backend/app/Console/Commands/.gitkeep +0 -0
  78. package/templates/app/backend/app/Events/.gitkeep +0 -0
  79. package/templates/app/backend/app/Http/Controllers/Admin/DashboardController.js +34 -0
  80. package/templates/app/backend/app/Http/Controllers/Admin/UserController.js +53 -0
  81. package/templates/app/backend/app/Http/Controllers/Auth/ApiTokenController.js +47 -0
  82. package/templates/app/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.js +41 -0
  83. package/templates/app/backend/app/Http/Controllers/Auth/ConfirmablePasswordController.js +14 -0
  84. package/templates/app/backend/app/Http/Controllers/Auth/EmailVerificationController.js +23 -0
  85. package/templates/app/backend/app/Http/Controllers/Auth/NewPasswordController.js +29 -0
  86. package/templates/app/backend/app/Http/Controllers/Auth/PasswordResetLinkController.js +20 -0
  87. package/templates/app/backend/app/Http/Controllers/Auth/RegisteredUserController.js +19 -0
  88. package/templates/app/backend/app/Http/Controllers/Auth/SessionController.js +36 -0
  89. package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorChallengeController.js +41 -0
  90. package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorController.js +63 -0
  91. package/templates/app/backend/app/Http/Controllers/ProfileController.js +66 -0
  92. package/templates/app/backend/app/Http/Kernel.js +8 -0
  93. package/templates/app/backend/app/Http/Middleware/Authenticate.js +84 -0
  94. package/templates/app/backend/app/Http/Middleware/CheckAbilities.js +17 -0
  95. package/templates/app/backend/app/Http/Middleware/CheckForAnyAbility.js +12 -0
  96. package/templates/app/backend/app/Http/Middleware/EnsureEmailIsVerified.js +11 -0
  97. package/templates/app/backend/app/Http/Middleware/EnsureHasRole.js +11 -0
  98. package/templates/app/backend/app/Http/Middleware/EnsureIsAdmin.js +11 -0
  99. package/templates/app/backend/app/Http/Middleware/RequirePassword.js +15 -0
  100. package/templates/app/backend/app/Http/Requests/Auth/LoginRequest.js +45 -0
  101. package/templates/app/backend/app/Http/Resources/.gitkeep +1 -0
  102. package/templates/app/backend/app/Jobs/.gitkeep +0 -0
  103. package/templates/app/backend/app/Listeners/.gitkeep +0 -0
  104. package/templates/app/backend/app/Mail/.gitkeep +0 -0
  105. package/templates/app/backend/app/Models/PersonalAccessToken.js +21 -0
  106. package/templates/app/backend/app/Models/User.js +64 -0
  107. package/templates/app/backend/app/Notifications/.gitkeep +0 -0
  108. package/templates/app/backend/app/Observers/.gitkeep +0 -0
  109. package/templates/app/backend/app/Policies/.gitkeep +0 -0
  110. package/templates/app/backend/app/Providers/AppServiceProvider.js +12 -0
  111. package/templates/app/backend/app/Rules/.gitkeep +1 -0
  112. package/templates/app/backend/config/app.js +16 -0
  113. package/templates/app/backend/config/auth.js +34 -0
  114. package/templates/app/backend/config/database.js +38 -0
  115. package/templates/app/backend/config/redis.js +28 -0
  116. package/templates/app/backend/config/session.js +36 -0
  117. package/templates/app/backend/database/migrations/0001_01_01_000000_create_users_table.js +49 -0
  118. package/templates/app/backend/database/migrations/0001_01_01_000001_create_cache_table.js +13 -0
  119. package/templates/app/backend/database/migrations/0001_01_01_000002_create_jobs_table.js +26 -0
  120. package/templates/app/backend/database/migrations/0001_01_01_000003_create_notifications_table.js +18 -0
  121. package/templates/app/backend/database/seeders/DatabaseSeeder.js +15 -0
  122. package/templates/app/backend/lang/ar/messages.json +10 -0
  123. package/templates/app/backend/lang/ar/validation.json +21 -0
  124. package/templates/app/backend/lang/en/messages.json +10 -0
  125. package/templates/app/backend/lang/en/validation.json +19 -0
  126. package/templates/app/backend/routes/api.js +25 -0
  127. package/templates/app/backend/routes/auth.js +52 -0
  128. package/templates/app/backend/routes/console.js +15 -0
  129. package/templates/app/frontend/App.kreftjs +4 -0
  130. package/templates/app/frontend/components/Field.kreftjs +19 -0
  131. package/templates/app/frontend/components/StatCard.kreftjs +47 -0
  132. package/templates/app/frontend/components/profile/ApiTokens.kreftjs +70 -0
  133. package/templates/app/frontend/components/profile/Sessions.kreftjs +42 -0
  134. package/templates/app/frontend/components/profile/TwoFactor.kreftjs +90 -0
  135. package/templates/app/frontend/errors/ErrorMonitor.kreftjs +264 -0
  136. package/templates/app/frontend/index.html +79 -0
  137. package/templates/app/frontend/kreftjs.d.ts +579 -0
  138. package/templates/app/frontend/kreftjs.js +5108 -0
  139. package/templates/app/frontend/layouts/app.kreftjs +718 -0
  140. package/templates/app/frontend/layouts/docs.kreftjs +37 -0
  141. package/templates/app/frontend/layouts/guest.kreftjs +18 -0
  142. package/templates/app/frontend/pages/Dashboard.kreftjs +273 -0
  143. package/templates/app/frontend/pages/ErrorMonitor.kreftjs +96 -0
  144. package/templates/app/frontend/pages/Home.kreftjs +617 -0
  145. package/templates/app/frontend/pages/Profile.kreftjs +104 -0
  146. package/templates/app/frontend/pages/Users.kreftjs +240 -0
  147. package/templates/app/frontend/pages/auth/ConfirmPassword.kreftjs +33 -0
  148. package/templates/app/frontend/pages/auth/ForgotPassword.kreftjs +36 -0
  149. package/templates/app/frontend/pages/auth/Login.kreftjs +89 -0
  150. package/templates/app/frontend/pages/auth/Register.kreftjs +48 -0
  151. package/templates/app/frontend/pages/auth/ResetPassword.kreftjs +45 -0
  152. package/templates/app/frontend/pages/auth/VerifyEmail.kreftjs +59 -0
  153. package/templates/app/frontend/routes/web.kreftjs +34 -0
  154. package/templates/app/frontend/tests/components/Counter.kreftjs +15 -0
  155. package/templates/app/frontend/tests/pages/About.kreftjs +12 -0
  156. package/templates/app/frontend/tests/pages/Test.kreftjs +31 -0
  157. package/templates/app/frontend/ui.css +517 -0
  158. package/templates/app/lang/ar/auth.json +6 -0
  159. package/templates/app/lang/ar/messages.json +6 -0
  160. package/templates/app/lang/ar/pagination.json +5 -0
  161. package/templates/app/lang/ar/passwords.json +8 -0
  162. package/templates/app/lang/ar/validation.json +22 -0
  163. package/templates/app/lang/ar.json +10 -0
  164. package/templates/app/lang/en/auth.json +6 -0
  165. package/templates/app/lang/en/messages.json +6 -0
  166. package/templates/app/lang/en/pagination.json +5 -0
  167. package/templates/app/lang/en/passwords.json +8 -0
  168. package/templates/app/lang/en/validation.json +22 -0
  169. package/templates/app/lang/en.json +10 -0
  170. package/templates/errors/401.kreftjs +11 -0
  171. package/templates/errors/402.kreftjs +11 -0
  172. package/templates/errors/403.kreftjs +11 -0
  173. package/templates/errors/404.kreftjs +11 -0
  174. package/templates/errors/419.kreftjs +11 -0
  175. package/templates/errors/429.kreftjs +11 -0
  176. package/templates/errors/500.kreftjs +11 -0
  177. package/templates/errors/503.kreftjs +11 -0
  178. package/templates/errors/ErrorMonitor.kreftjs +502 -0
  179. package/templates/errors/error.kreftjs +11 -0
  180. package/templates/errors/minimal.kreftjs +25 -0
  181. package/tools/dev-client.js +125 -0
  182. package/tools/devtools.js +172 -0
  183. package/tools/vite-plugin-kreftjs.d.ts +52 -0
  184. package/tools/vite-plugin-kreftjs.js +466 -0
package/server.js ADDED
@@ -0,0 +1,8 @@
1
+ /*
2
+ * Kreft on the server: routes, controllers, models, migrations, seeders, middleware and namespaces.
3
+ *
4
+ * const { Route, Model, DB, Seeder, abort, response, use, namespace } = require('kreftjs/server')
5
+ *
6
+ * `kreftjs dev` and `kreftjs serve` load routes/api.js and answer /api/... with it.
7
+ */
8
+ module.exports = require('./bin/backend/index.js');
@@ -0,0 +1,127 @@
1
+ # My Kreft Application
2
+
3
+ Welcome to your **Kreft** full-stack application.
4
+
5
+ ## Quick Commands
6
+
7
+ ```bash
8
+ npm run dev # Start dev server with hot-reload + backend API (http://localhost:8000)
9
+ npm run build # Compile minified production assets into dist/
10
+ npm run serve # Run the production server (serves dist/ + backend/ API)
11
+ npm test # Run backend & frontend test suites
12
+ ```
13
+
14
+ > **Demo Credentials:** Sign in with **test@example.com** and password **password**, or register a new account. Outgoing emails (verification, password reset) are printed in the terminal until `MAIL_URL` is configured in `.env`.
15
+
16
+ ---
17
+
18
+ ## Project Architecture
19
+
20
+ Your project cleanly isolates client-side UI (`frontend/`) from server-side business logic (`backend/`):
21
+
22
+ ```text
23
+ ├── frontend/ # Public client application (the only folder served to browsers)
24
+ │ ├── index.html # HTML entry, auth store, router & app bootstrap
25
+ │ ├── App.kreftjs # Root reactive shell
26
+ │ ├── routes/web.kreftjs # Client-side routes (Dashboard, Users, Profile, Auth)
27
+ │ ├── layouts/ # app.kreftjs (authenticated shell) & guest.kreftjs (auth pages)
28
+ │ ├── pages/ # Route views (Home, Dashboard, Users, Profile, auth/*)
29
+ │ ├── components/ # Reusable widgets (Field.kreftjs, StatCard.kreftjs, profile/*)
30
+ │ ├── errors/ # Custom HTTP error views (404.kreftjs, 500.kreftjs, ErrorMonitor.kreftjs)
31
+ │ ├── docs/ # Built-in documentation (accessible at /docs)
32
+ │ └── ui.css # Design system tokens & component styles
33
+ │
34
+ ├── backend/ # Private server engine (never exposed or built into dist/)
35
+ │ ├── config/ # Centralized config (app, auth, database, redis, session)
36
+ │ ├── routes/ # api.js (REST API), auth.js (Auth endpoints), console.js (Cron)
37
+ │ ├── app/
38
+ │ │ ├── Http/Controllers/ # Request handlers & validation
39
+ │ │ ├── Http/Middleware/ # Auth, CheckAbilities, EnsureIsAdmin, security headers
40
+ │ │ └── Models/ # Active Record models (User.js, PersonalAccessToken.js, etc.)
41
+ │ └── database/ # migrations/, seeders/, factories/, and SQLite app.db
42
+ │
43
+ └── .env # Environment secrets (APP_KEY, DB_*, REDIS_*, SESSION_*)
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Scaffolding & CLI Generators
49
+
50
+ Generate full-stack features from the terminal:
51
+
52
+ ```bash
53
+ # Generate Model + Migration + Factory + Seeder + Controller + .kreftjs Page + Route:
54
+ kreftjs make:model Post --all
55
+
56
+ # Or pick specific layers (-m migration, -f factory, -s seeder, -c controller, -p page, -r route):
57
+ kreftjs make:model Post -mfscpr --route=api --auth
58
+
59
+ # Individual generators:
60
+ kreftjs make:controller OrderController --api
61
+ kreftjs make:request StorePostRequest --model Post
62
+ kreftjs make:resource PostResource
63
+ kreftjs make:rule StrongPassword
64
+ kreftjs make:observer PostObserver --model Post
65
+ kreftjs make:middleware EnsureSubscribed
66
+ kreftjs make:job ProcessWebhook
67
+ kreftjs make:mail InvoiceReady
68
+ kreftjs make:routes billing # Creates frontend/routes/billing.kreftjs & backend/routes/billing.js
69
+
70
+ # Database & inspection tools:
71
+ kreftjs db:migrate # Run pending migrations
72
+ kreftjs db:seed # Run database seeders
73
+ kreftjs route:list # List all registered backend API routes
74
+ kreftjs redis:ping # Verify Redis connectivity
75
+ kreftjs tinker # Interactive REPL with your Models, DB, Cache & Redis preloaded
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Writing `.kreftjs` Components
81
+
82
+ Every `.kreftjs` file combines reactive state, actions, `<view>`, and optional `<style scoped>`:
83
+
84
+ ```html
85
+ <!-- frontend/pages/Posts.kreftjs -->
86
+ state title = ''
87
+ load posts = api.get('/api/posts')
88
+
89
+ action create() {
90
+ await api.post('/api/posts', { title })
91
+ title = ''
92
+ posts.reload()
93
+ }
94
+
95
+ <view>
96
+ <h1>Posts</h1>
97
+ <form @submit.prevent={create} class="row">
98
+ <input k-model="title" placeholder="Post title" required>
99
+ <button class="primary">Publish</button>
100
+ </form>
101
+
102
+ <ul k-if="posts.data">
103
+ {for post in posts.data key post.id}
104
+ <li>{post.title}</li>
105
+ {/for}
106
+ </ul>
107
+ </view>
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Installing & Updating Packages
113
+
114
+ Use `kreftjs add` to install npm packages safely (blocks untrusted lifecycle scripts, checks for typosquatting, and skips redundant packages already built into Kreft):
115
+
116
+ ```bash
117
+ kreftjs add dayjs nanoid
118
+ kreftjs add -D esbuild # Bundles browser packages into import-mapped modules
119
+ ```
120
+
121
+ Keep dependencies and your embedded Kreft runtime up to date:
122
+
123
+ ```bash
124
+ kreftjs update # Update npm dependencies & refresh kreftjs.js / kreftjs.d.ts
125
+ kreftjs update --check # Check for outdated packages without modifying files
126
+ kreftjs update --core # Refresh only Kreft runtime & TypeScript definitions
127
+ ```
@@ -0,0 +1,64 @@
1
+ # Settings for this app. Copy this file to .env and fill it in.
2
+ # .env is never sent to the browser, never copied by kreftjs build, and not committed (see .gitignore).
3
+ # Server code (routes/, app/, database/) reads it as process.env.NAME.
4
+ APP_NAME="My app"
5
+
6
+ # Signs the links in emails. kreftjs new makes one; keep it secret, and keep it the same once users have links.
7
+ APP_KEY=
8
+ # Where the app runs: links in emails start with this
9
+ APP_URL=http://localhost:8000
10
+
11
+ # Only values that start with PUBLIC_ also reach the browser: {env.PUBLIC_APP_NAME} in any .kreftjs file.
12
+ # Never put a password or a secret key in a PUBLIC_ value.
13
+ PUBLIC_APP_NAME="${APP_NAME}"
14
+
15
+ # The database: sqlite:file.db, mysql://user:password@host:3306/name or postgres://user:password@host:5432/name
16
+ DATABASE_URL=sqlite:backend/database/app.db
17
+
18
+ # Background work: queued jobs, mail and notifications (redis, database: the jobs table, run by kreftjs dev / kreftjs queue:work; sync: at once)
19
+ QUEUE_CONNECTION=database
20
+ # Cache.put / Cache.remember: redis, database, file or memory
21
+ CACHE_STORE=database
22
+ # Storage.put: local (backend/storage/app, private) or public (served at /storage)
23
+ FILESYSTEM_DISK=local
24
+
25
+ # Redis (for Cache, Queue, and pub/sub): redis://default:secret@127.0.0.1:6379/0 or host/port
26
+ # REDIS_URL=redis://127.0.0.1:6379/0
27
+ # REDIS_HOST=127.0.0.1
28
+ # REDIS_PORT=6379
29
+ # REDIS_PASSWORD=
30
+ # REDIS_DB=0
31
+ # REDIS_PREFIX=som:
32
+ # Environment & Debugging:
33
+ # - local: plain HTTP allowed on localhost, interactive bug diagnostics & stack traces shown
34
+ # - production: plain HTTP disallowed for security (enforces HTTPS & HSTS), bug details & stack traces hidden
35
+ APP_ENV=local
36
+ APP_DEBUG=true
37
+
38
+ # Force all traffic over secure HTTPS in production (auto-redirects HTTP -> HTTPS with HSTS)
39
+ FORCE_HTTPS=false
40
+ SESSION_SECURE_COOKIE=false
41
+
42
+ # Security & Sessions
43
+ # A login token nobody used for this many days stops working
44
+ AUTH_TOKEN_DAYS=30
45
+ # wrong passwords allowed per account (in LOGIN_WINDOW_SECONDS) before logins for it are refused, and wrong two-factor codes
46
+ LOGIN_MAX_FAILURES=10
47
+ LOGIN_WINDOW_SECONDS=900
48
+ TWO_FACTOR_MAX_FAILURES=8
49
+ # Session lifetime in minutes (backend inactivity timeout)
50
+ SESSION_LIFETIME=120
51
+ SESSION_EXPIRE_ON_CLOSE=false
52
+ SESSION_DRIVER=file
53
+
54
+ # Frontend Inactivity Timer in minutes (auto-redirect to /login after inactivity)
55
+ # Values starting with PUBLIC_ are sent to the browser and readable as {env.PUBLIC_...} in .kreftjs files
56
+ PUBLIC_SESSION_TIMEOUT=15
57
+ PUBLIC_SESSION_WARNING=2
58
+
59
+ # Behind a proxy or load balancer (nginx, Cloudflare…), its address, so the visitor's real IP is used: 10.0.0.1 or *
60
+ # TRUSTED_PROXIES=
61
+
62
+ # Email. Without MAIL_URL, emails (verify email, reset password) are printed in the terminal.
63
+ # MAIL_URL=smtp://user:password@smtp.example.com:587 (and: npm i nodemailer)
64
+ # MAIL_FROM="My app <hello@example.com>"
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ backend/database/*.db*
5
+ backend/storage/
@@ -0,0 +1,9 @@
1
+ {
2
+ "files.associations": {
3
+ "*.kreftjs": "kreft"
4
+ },
5
+ "[kreft]": {
6
+ "editor.defaultFormatter": "kreft.kreftjs",
7
+ "editor.formatOnSave": true
8
+ }
9
+ }
@@ -0,0 +1,177 @@
1
+ // Signing a browser in and out via secure SPA session cookies.
2
+ //
3
+ // A browser gets its login in an HttpOnly cookie (kreft_session): page scripts can't read it, so a
4
+ // script injected into a page can't steal it. Changes (POST, PUT, DELETE…) also need the X-XSRF-TOKEN
5
+ // header to match the XSRF-TOKEN cookie, which only your own pages can read (Kreftjs.api sends it by itself).
6
+ //
7
+ // An app or script that sends `device_name` when it logs in gets an API token in the answer instead:
8
+ // Authorization: Bearer 5|k3J9…
9
+ const { response, Str, Crypt, cookieSecure } = require('kreftjs/server')
10
+ const PersonalAccessToken = require('../Models/PersonalAccessToken')
11
+
12
+ const SESSION = 'kreft_session'
13
+ const XSRF = 'XSRF-TOKEN'
14
+ const days = () => Number(process.env.AUTH_TOKEN_DAYS || 30)
15
+
16
+ const resolveSecure = () => typeof cookieSecure === 'function' ? cookieSecure() : (process.env.SESSION_SECURE_COOKIE ? process.env.SESSION_SECURE_COOKIE === 'true' : (process.env.APP_ENV === 'production' || /^https:/i.test(process.env.APP_URL || '')))
17
+ const resolveSameSite = () => process.env.SESSION_SAME_SITE || 'Lax'
18
+
19
+ // the answer to a successful login or register
20
+ async function signIn(req, user, { remember = false, status = 200 } = {}) {
21
+ const deviceName = req.input('device_name')
22
+ if (deviceName) { // an app or a script: a Bearer token
23
+ const { plainTextToken } = await user.createToken(deviceName, { type: 'api', ip: req.ip(), userAgent: req.header('user-agent') })
24
+ return response({ user, token: plainTextToken }, status)
25
+ }
26
+
27
+ // Session fixation prevention: invalidate any previous session in this browser
28
+ const fromCookie = req.cookie(SESSION)
29
+ if (fromCookie) {
30
+ try {
31
+ const oldPayload = Crypt.decryptString(fromCookie)
32
+ const [oldId] = String(oldPayload || '').split('|')
33
+ if (oldId) {
34
+ await PersonalAccessToken.where('id', oldId).delete()
35
+ }
36
+ } catch (_) {
37
+ // ignore invalid / rotated old session tokens
38
+ }
39
+ }
40
+
41
+ // Generate a completely new session identifier and token
42
+ const { plainTextToken } = await user.createToken(browserName(req.header('user-agent', '')), {
43
+ type: 'session', ip: req.ip(), userAgent: req.header('user-agent'), remember,
44
+ })
45
+ const res = response({ user }, status)
46
+ // "Remember me": the cookie stays for AUTH_TOKEN_DAYS; otherwise it goes when the browser closes
47
+ // Cryptographically seal session token with APP_KEY (AES-256-GCM with fresh 12-byte random IV).
48
+ // If APP_KEY is changed or rotated, existing session cookies fail authentication and are destroyed.
49
+ const encryptedSession = Crypt.encryptString(plainTextToken)
50
+ const isSecure = resolveSecure()
51
+ const sameSite = resolveSameSite()
52
+
53
+ res.cookie(SESSION, encryptedSession, {
54
+ httpOnly: true,
55
+ secure: isSecure,
56
+ sameSite,
57
+ path: '/',
58
+ maxAge: remember ? days() * 86400 : undefined,
59
+ })
60
+ res.cookie(XSRF, Str.random(40), {
61
+ httpOnly: false,
62
+ secure: isSecure,
63
+ sameSite,
64
+ path: '/',
65
+ maxAge: remember ? days() * 86400 : undefined,
66
+ })
67
+ return res
68
+ }
69
+
70
+ // Regenerates the session token (Session Fixation Prevention on privilege change)
71
+ async function regenerate(req, res, user = null) {
72
+ const targetUser = user || req.user
73
+ if (!targetUser) return res
74
+
75
+ if (req.accessToken) {
76
+ await req.accessToken.delete()
77
+ } else if (req.cookie(SESSION)) {
78
+ try {
79
+ const oldPayload = Crypt.decryptString(req.cookie(SESSION))
80
+ const [oldId] = String(oldPayload || '').split('|')
81
+ if (oldId) await PersonalAccessToken.where('id', oldId).delete()
82
+ } catch (_) {}
83
+ }
84
+
85
+ const { accessToken, plainTextToken } = await targetUser.createToken(browserName(req.header('user-agent', '')), {
86
+ type: 'session', ip: req.ip(), userAgent: req.header('user-agent'), remember: req.accessToken ? req.accessToken.remember : false,
87
+ })
88
+ req.accessToken = accessToken
89
+
90
+ const encryptedSession = Crypt.encryptString(plainTextToken)
91
+ const isSecure = resolveSecure()
92
+ const sameSite = resolveSameSite()
93
+ const remember = accessToken.remember
94
+
95
+ res.cookie(SESSION, encryptedSession, {
96
+ httpOnly: true,
97
+ secure: isSecure,
98
+ sameSite,
99
+ path: '/',
100
+ maxAge: remember ? days() * 86400 : undefined,
101
+ })
102
+ res.cookie(XSRF, Str.random(40), {
103
+ httpOnly: false,
104
+ secure: isSecure,
105
+ sameSite,
106
+ path: '/',
107
+ maxAge: remember ? days() * 86400 : undefined,
108
+ })
109
+ return res
110
+ }
111
+
112
+ function signOut(res) {
113
+ return res.withoutCookie(SESSION).withoutCookie(XSRF)
114
+ }
115
+
116
+ // two-factor: after the password, the login waits (5 minutes) for the code in an encrypted cookie
117
+ const usedTwoFactorTickets = new Map()
118
+ const twoFactorAttempts = new Map()
119
+ setInterval(() => {
120
+ const now = Date.now()
121
+ for (const [k, exp] of usedTwoFactorTickets) if (exp < now) usedTwoFactorTickets.delete(k)
122
+ for (const [k, v] of twoFactorAttempts) if (v.exp < now) twoFactorAttempts.delete(k)
123
+ }, 60000).unref()
124
+
125
+ function pendingTwoFactor(req, user, remember) {
126
+ const nonce = Str.random(24)
127
+ const exp = Date.now() + 5 * 60000
128
+ const ticket = Crypt.encrypt({ id: user.id, remember: Boolean(remember), nonce, exp })
129
+ if (req.input('device_name')) return response({ two_factor: true, challenge: ticket })
130
+ return response({ two_factor: true }).cookie('kreft_2fa', ticket, { maxAge: 300 })
131
+ }
132
+ function readPendingTwoFactor(req) {
133
+ const ticket = req.input('challenge') || req.cookie('kreft_2fa')
134
+ if (!ticket) return null
135
+ try {
136
+ const t = Crypt.decrypt(ticket)
137
+ if (!t || !(t.exp > Date.now())) return null
138
+ if (t.nonce && usedTwoFactorTickets.has(t.nonce)) return null
139
+ if (t.nonce) {
140
+ const state = twoFactorAttempts.get(t.nonce) || { count: 0, exp: t.exp }
141
+ if (state.count >= 5) {
142
+ if (usedTwoFactorTickets.size >= 10000) usedTwoFactorTickets.delete(usedTwoFactorTickets.keys().next().value)
143
+ usedTwoFactorTickets.set(t.nonce, t.exp)
144
+ return null
145
+ }
146
+ }
147
+ return t
148
+ } catch (_) { return null }
149
+ }
150
+ function recordTwoFactorFailure(pending) {
151
+ if (!pending || !pending.nonce) return
152
+ const state = twoFactorAttempts.get(pending.nonce) || { count: 0, exp: pending.exp }
153
+ state.count++
154
+ if (!twoFactorAttempts.has(pending.nonce) && twoFactorAttempts.size >= 10000) {
155
+ twoFactorAttempts.delete(twoFactorAttempts.keys().next().value)
156
+ }
157
+ twoFactorAttempts.set(pending.nonce, state)
158
+ if (state.count >= 5) {
159
+ if (usedTwoFactorTickets.size >= 10000) usedTwoFactorTickets.delete(usedTwoFactorTickets.keys().next().value)
160
+ usedTwoFactorTickets.set(pending.nonce, pending.exp)
161
+ }
162
+ }
163
+ function consumeTwoFactorTicket(pending) {
164
+ if (!pending || !pending.nonce) return
165
+ if (usedTwoFactorTickets.size >= 10000) usedTwoFactorTickets.delete(usedTwoFactorTickets.keys().next().value)
166
+ usedTwoFactorTickets.set(pending.nonce, pending.exp)
167
+ twoFactorAttempts.delete(pending.nonce)
168
+ }
169
+
170
+ // "Chrome on macOS", for the list of devices
171
+ function browserName(ua) {
172
+ const b = /Edg\//.test(ua) ? 'Edge' : /OPR\//.test(ua) ? 'Opera' : /Firefox\//.test(ua) ? 'Firefox' : /Chrome\//.test(ua) ? 'Chrome' : /Safari\//.test(ua) ? 'Safari' : /curl|node|axios|python/i.test(ua) ? 'Script' : 'Browser'
173
+ const os = /iPhone|iPad/.test(ua) ? 'iOS' : /Android/.test(ua) ? 'Android' : /Mac OS X/.test(ua) ? 'macOS' : /Windows/.test(ua) ? 'Windows' : /Linux/.test(ua) ? 'Linux' : ''
174
+ return os ? `${b} on ${os}` : b
175
+ }
176
+
177
+ module.exports = { signIn, signOut, regenerate, pendingTwoFactor, readPendingTwoFactor, recordTwoFactorFailure, consumeTwoFactorTicket, browserName, SESSION, XSRF }
@@ -0,0 +1,29 @@
1
+ // Failed-attempt counters kept per account (not per address), in the Cache so every process shares them.
2
+ // The 'throttle:6,1' route limit counts per address; this one stops a guesser who changes address:
3
+ // const wait = await Throttle.wait('login', email, 10) // seconds to wait, 0 when allowed
4
+ // if (wait) abort(429, …) … await Throttle.hit('login', email, 900) … await Throttle.clear('login', email)
5
+ const { Cache, Hash } = require('kreftjs/server')
6
+
7
+ const key = (scope, id) => `throttle:${scope}:${Hash.sha256(String(id).trim().toLowerCase())}`
8
+
9
+ // seconds until this account may try again, or 0
10
+ async function wait(scope, id, max) {
11
+ const s = await Cache.get(key(scope, id))
12
+ if (!s || !(s.resetAt > Date.now()) || s.count < max) return 0
13
+ return Math.max(1, Math.ceil((s.resetAt - Date.now()) / 1000))
14
+ }
15
+
16
+ // one more failure, counted for `windowSeconds` from the first one
17
+ async function hit(scope, id, windowSeconds = 900) {
18
+ const k = key(scope, id)
19
+ const s = await Cache.get(k)
20
+ const cur = s && s.resetAt > Date.now() ? s : { count: 0, resetAt: Date.now() + windowSeconds * 1000 }
21
+ cur.count++
22
+ await Cache.put(k, cur, Math.max(1, Math.ceil((cur.resetAt - Date.now()) / 1000)))
23
+ }
24
+
25
+ async function clear(scope, id) {
26
+ await Cache.forget(key(scope, id))
27
+ }
28
+
29
+ module.exports = { wait, hit, clear }
File without changes
@@ -0,0 +1,34 @@
1
+ const User = require('../../../Models/User')
2
+ const PersonalAccessToken = require('../../../Models/PersonalAccessToken')
3
+
4
+ // GET /api/admin/stats (frontend/pages/Dashboard.kreftjs)
5
+ class DashboardController {
6
+ async index(req) {
7
+ if (!req.user || !req.user.is_admin) {
8
+ return {
9
+ users: 1,
10
+ admins: 0,
11
+ unverified: req.user && !req.user.email_verified_at ? 1 : 0,
12
+ newThisWeek: 0,
13
+ signedIn: req.user ? await PersonalAccessToken.where('user_id', req.user.id).count() : 0,
14
+ latest: req.user ? [req.user] : [],
15
+ }
16
+ }
17
+ const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 19).replace('T', ' ')
18
+ return {
19
+ users: await User.count(),
20
+ admins: await User.where('is_admin', true).count(),
21
+ unverified: await User.query().whereNull('email_verified_at').count(),
22
+ newThisWeek: await User.where('created_at', '>=', weekAgo).count(),
23
+ signedIn: await PersonalAccessToken.count(),
24
+ latest: await User.latest('id').limit(5).get(),
25
+ }
26
+ }
27
+
28
+ // GET /api/admin/me (frontend/pages/Profile.kreftjs)
29
+ async me(req) {
30
+ return { user: req.user, devices: await PersonalAccessToken.where('user_id', req.user.id).count() }
31
+ }
32
+ }
33
+
34
+ module.exports = DashboardController
@@ -0,0 +1,53 @@
1
+ const { abort, noContent } = require('kreftjs/server')
2
+ const User = require('../../../Models/User')
3
+ const PersonalAccessToken = require('../../../Models/PersonalAccessToken')
4
+
5
+ // the roles a user can have ('role:editor' in the routes); add your own
6
+ const ROLES = ['user', 'editor']
7
+
8
+ // /api/admin/users (frontend/pages/Users.kreftjs)
9
+ class UserController {
10
+ // GET /api/admin/users?q=ali&page=2
11
+ async index(req) {
12
+ if (!req.user || !req.user.is_admin) abort(403, 'Only admins can view user accounts.')
13
+ const q = req.string('q').trim()
14
+ const safeQ = q.replace(/[%_]/g, '')
15
+ return User.query()
16
+ .when(safeQ, (query) => query.where((w) => w.whereLike('name', `%${safeQ}%`).orWhere('email', 'like', `%${safeQ.toLowerCase()}%`)))
17
+ .orderByDesc('id')
18
+ .paginate(15, req.integer('page', 1))
19
+ }
20
+
21
+ // PATCH /api/admin/users/{id} { is_admin } and/or { role }
22
+ async update(req, id) {
23
+ if (!req.user || !req.user.is_admin) abort(403, 'Only admins can modify user roles or permissions.')
24
+ const user = await User.findOrFail(id)
25
+ const data = await req.validate({ is_admin: 'sometimes|boolean', role: `sometimes|in:${ROLES.join(',')}` })
26
+ if (data.is_admin !== undefined) {
27
+ const on = data.is_admin === true || data.is_admin === 1 || data.is_admin === '1' || data.is_admin === 'true'
28
+ const currentId = req.user ? req.user.id : null
29
+ if (currentId && user.id === currentId && !on) abort(422, "You can't take away your own admin rights.")
30
+ user.is_admin = on
31
+ }
32
+ if (data.role !== undefined) user.role = data.role
33
+ await user.save()
34
+ // Session Fixation & Privilege Protection: invalidate previous sessions when privileges change
35
+ if (data.is_admin !== undefined || data.role !== undefined) {
36
+ await PersonalAccessToken.where('user_id', user.id).where('type', 'session').delete()
37
+ }
38
+ return user
39
+ }
40
+
41
+ // DELETE /api/admin/users/{id}
42
+ async destroy(req, id) {
43
+ if (!req.user || !req.user.is_admin) abort(403, 'Only admins can delete user accounts.')
44
+ const user = await User.findOrFail(id)
45
+ const currentId = req.user ? req.user.id : null
46
+ if (currentId && user.id === currentId) abort(422, "You can't delete yourself here. Use your profile page.")
47
+ await PersonalAccessToken.where('user_id', user.id).delete()
48
+ await user.delete()
49
+ return noContent()
50
+ }
51
+ }
52
+
53
+ module.exports = UserController
@@ -0,0 +1,47 @@
1
+ const { noContent, abort, response } = require('kreftjs/server')
2
+ const PersonalAccessToken = require('../../../Models/PersonalAccessToken')
3
+
4
+ // What an API token may do: change this list for your app. 'abilities:posts:write' in a route checks it.
5
+ const ABILITIES = ['read', 'create', 'update', 'delete']
6
+
7
+ // API tokens for scripts and other apps (pages/Profile.kreftjs → API tokens), like Jetstream / Sanctum
8
+ class ApiTokenController {
9
+ // GET /api/user/tokens
10
+ async index(req) {
11
+ if (req.accessToken && req.accessToken.type !== 'session' && !req.accessToken.can('*')) abort(403, 'Only browser sessions may manage API tokens.')
12
+ const rows = await PersonalAccessToken.where('user_id', req.user.id).where('type', 'api').orderByDesc('id').get()
13
+ return { abilities: ABILITIES, tokens: rows.map((t) => ({ id: t.id, name: t.name, abilities: t.abilities || ['*'], last_used_at: t.last_used_at, created_at: t.created_at })) }
14
+ }
15
+
16
+ // POST /api/user/tokens { name, abilities: ['read'] } → the token, shown this one time only
17
+ async store(req) {
18
+ if (req.accessToken && req.accessToken.type !== 'session' && !req.accessToken.can('*')) abort(403, 'Only browser sessions may create API tokens.')
19
+ const data = await req.validate({ name: 'required|string|max:100', abilities: 'sometimes|array' })
20
+ const abilities = (data.abilities || ['read']).filter((a) => ABILITIES.includes(a))
21
+ if (!abilities.length) abort(422, 'Choose at least one permission.')
22
+ const { accessToken, plainTextToken } = await req.user.createToken(data.name, { type: 'api', abilities })
23
+ return response({ id: accessToken.id, name: accessToken.name, abilities, token: plainTextToken }, 201)
24
+ }
25
+
26
+ // PUT /api/user/tokens/{id} { abilities }
27
+ async update(req, id) {
28
+ if (req.accessToken && req.accessToken.type !== 'session' && !req.accessToken.can('*')) abort(403, 'Only browser sessions may update API tokens.')
29
+ const token = await PersonalAccessToken.where('user_id', req.user.id).where('type', 'api').find(id)
30
+ if (!token) abort(404, 'No such token.')
31
+ const { abilities } = await req.validate({ abilities: 'required|array' })
32
+ token.abilities = abilities.filter((a) => ABILITIES.includes(a))
33
+ await token.save()
34
+ return { id: token.id, abilities: token.abilities }
35
+ }
36
+
37
+ // DELETE /api/user/tokens/{id}
38
+ async destroy(req, id) {
39
+ if (req.accessToken && req.accessToken.type !== 'session' && !req.accessToken.can('*')) abort(403, 'Only browser sessions may delete API tokens.')
40
+ const token = await PersonalAccessToken.where('user_id', req.user.id).where('type', 'api').find(id)
41
+ if (!token) abort(404, 'No such token.')
42
+ await token.delete()
43
+ return noContent()
44
+ }
45
+ }
46
+
47
+ module.exports = ApiTokenController
@@ -0,0 +1,41 @@
1
+ const { Hash, ValidationError, noContent, abort } = require('kreftjs/server')
2
+ const User = require('../../../Models/User')
3
+ const Throttle = require('../../../Auth/Throttle')
4
+
5
+ const MAX_FAILURES = Number(process.env.LOGIN_MAX_FAILURES || 10) // wrong passwords per account ...
6
+ const WINDOW_SECONDS = Number(process.env.LOGIN_WINDOW_SECONDS || 900) // ... in this many seconds
7
+ const LoginRequest = require('../../Requests/Auth/LoginRequest')
8
+ const { signIn, signOut, pendingTwoFactor } = require('../../../Auth/Session')
9
+
10
+ class AuthenticatedSessionController {
11
+ // POST /api/login { email, password, remember } (pages/auth/Login.kreftjs)
12
+ // with two-factor on, the answer is { two_factor: true } and the code goes to /api/two-factor-challenge
13
+ async store(req) {
14
+ const data = await LoginRequest.validate(req)
15
+ const email = data.email.toLowerCase()
16
+ // per account, whatever the address: guessing from many addresses at once stops here (known or not, same count)
17
+ const locked = await Throttle.wait('login', email, MAX_FAILURES)
18
+ if (locked) abort(429, 'Too many failed attempts. Try again later.', { headers: { 'Retry-After': String(locked) } })
19
+ const user = await User.where('email', email).first()
20
+ if (!user) {
21
+ Hash.dummyCheck(data.password)
22
+ await Throttle.hit('login', email, WINDOW_SECONDS)
23
+ throw new ValidationError({ email: ['These credentials do not match our records.'] })
24
+ }
25
+ if (!Hash.check(data.password, user.password)) {
26
+ await Throttle.hit('login', email, WINDOW_SECONDS)
27
+ throw new ValidationError({ email: ['These credentials do not match our records.'] })
28
+ }
29
+ await Throttle.clear('login', email)
30
+ if (user.hasTwoFactor()) return pendingTwoFactor(req, user, req.boolean('remember'))
31
+ return signIn(req, user, { remember: req.boolean('remember') })
32
+ }
33
+
34
+ // POST /api/logout: this device's login stops working
35
+ async destroy(req) {
36
+ await req.accessToken.delete()
37
+ return signOut(noContent())
38
+ }
39
+ }
40
+
41
+ module.exports = AuthenticatedSessionController
@@ -0,0 +1,14 @@
1
+ const { Hash, ValidationError } = require('kreftjs/server')
2
+
3
+ class ConfirmablePasswordController {
4
+ // POST /api/confirm-password (pages/auth/ConfirmPassword.kreftjs): opens 'password.confirm' routes for 3 hours
5
+ async store(req) {
6
+ const { password } = await req.validate({ password: 'required' })
7
+ if (!Hash.check(password, req.user.password)) throw new ValidationError({ password: ['The password is incorrect.'] })
8
+ req.accessToken.password_confirmed_at = new Date()
9
+ await req.accessToken.save()
10
+ return { confirmed: true }
11
+ }
12
+ }
13
+
14
+ module.exports = ConfirmablePasswordController
@@ -0,0 +1,23 @@
1
+ const { Hash, Url, abort, same } = require('kreftjs/server')
2
+ const User = require('../../../Models/User')
3
+
4
+ class EmailVerificationController {
5
+ // POST /api/email/verification-notification: send the link again (pages/auth/VerifyEmail.kreftjs)
6
+ async send(req) {
7
+ if (req.user.hasVerifiedEmail()) return { message: 'Your email address is already verified.' }
8
+ await req.user.sendEmailVerificationNotification()
9
+ return { message: 'A new verification link has been sent to your email address.' }
10
+ }
11
+
12
+ // POST /api/email/verify { id, hash, expires, signature }: the link from the email was opened
13
+ async verify(req) {
14
+ const { id, hash, expires, signature } = req.all()
15
+ if (!Url.check(`/verify-email/${id}/${hash}`, { expires, signature })) abort(403, 'This link is not valid any more. Ask for a new one.')
16
+ const user = await User.findOrFail(id)
17
+ if (!same(String(hash || ''), Hash.sha256(user.email))) abort(403, 'This link is not valid any more. Ask for a new one.')
18
+ if (!user.hasVerifiedEmail()) await user.markEmailAsVerified()
19
+ return { message: 'Your email address is verified.', user: { id: user.id, email_verified_at: user.email_verified_at } }
20
+ }
21
+ }
22
+
23
+ module.exports = EmailVerificationController