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.
- package/README.md +103 -2
- package/bin/add.js +1074 -0
- package/bin/artisan-more.js +994 -0
- package/bin/artisan.js +1877 -0
- package/bin/backend/app.js +161 -0
- package/bin/backend/cache.js +196 -0
- package/bin/backend/collection.js +117 -0
- package/bin/backend/command.js +45 -0
- package/bin/backend/config.js +234 -0
- package/bin/backend/error-page.js +773 -0
- package/bin/backend/events.js +49 -0
- package/bin/backend/factory.js +223 -0
- package/bin/backend/fake.js +180 -0
- package/bin/backend/form.js +102 -0
- package/bin/backend/gate.js +79 -0
- package/bin/backend/gc.js +69 -0
- package/bin/backend/html.js +322 -0
- package/bin/backend/http.js +652 -0
- package/bin/backend/index.js +452 -0
- package/bin/backend/mail.js +77 -0
- package/bin/backend/mcp.js +433 -0
- package/bin/backend/migrator.js +370 -0
- package/bin/backend/model.js +781 -0
- package/bin/backend/namespace.js +213 -0
- package/bin/backend/notifications.js +68 -0
- package/bin/backend/qr.js +164 -0
- package/bin/backend/query.js +387 -0
- package/bin/backend/queue.js +412 -0
- package/bin/backend/redis.js +479 -0
- package/bin/backend/schedule.js +112 -0
- package/bin/backend/schema.js +321 -0
- package/bin/backend/seo-pages.js +100 -0
- package/bin/backend/storage.js +153 -0
- package/bin/backend/support.js +567 -0
- package/bin/backend/typescript.js +166 -0
- package/bin/build.js +540 -0
- package/bin/db-api.js +249 -0
- package/bin/db.js +275 -0
- package/bin/deps.js +138 -0
- package/bin/electron.js +509 -0
- package/bin/env.js +97 -0
- package/bin/guard.js +80 -0
- package/bin/kreftjs-config.js +40 -0
- package/bin/kreftjs.js +1373 -0
- package/bin/lang.js +241 -0
- package/bin/layout.js +74 -0
- package/bin/no-auth.js +162 -0
- package/bin/prerender.js +258 -0
- package/bin/safe-json.js +24 -0
- package/bin/server-routes.js +154 -0
- package/bin/ssr.js +133 -0
- package/bin/terminal.js +195 -0
- package/bin/to-ts.js +36 -0
- package/bin/update.js +237 -0
- package/bin/urls-file.js +27 -0
- package/bin/vite.js +228 -0
- package/docs/Docs.kreftjs +557 -0
- package/docs/README.md +2302 -0
- package/docs/markdown.js +186 -0
- package/editors/vscode/README.md +13 -0
- package/editors/vscode/extension.js +183 -0
- package/editors/vscode/language-configuration.json +17 -0
- package/editors/vscode/package.json +57 -0
- package/editors/vscode/snippets/kreftjs.json +21 -0
- package/editors/vscode/syntaxes/kreftjs.tmLanguage.json +207 -0
- package/kreftjs.d.ts +579 -0
- package/kreftjs.js +5108 -0
- package/package.json +97 -4
- package/server.d.ts +1028 -0
- package/server.js +8 -0
- package/templates/app/README.md +127 -0
- package/templates/app/_env.example +64 -0
- package/templates/app/_gitignore +5 -0
- package/templates/app/_vscode/settings.json +9 -0
- package/templates/app/backend/app/Auth/Session.js +177 -0
- package/templates/app/backend/app/Auth/Throttle.js +29 -0
- package/templates/app/backend/app/Console/Commands/.gitkeep +0 -0
- package/templates/app/backend/app/Events/.gitkeep +0 -0
- package/templates/app/backend/app/Http/Controllers/Admin/DashboardController.js +34 -0
- package/templates/app/backend/app/Http/Controllers/Admin/UserController.js +53 -0
- package/templates/app/backend/app/Http/Controllers/Auth/ApiTokenController.js +47 -0
- package/templates/app/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.js +41 -0
- package/templates/app/backend/app/Http/Controllers/Auth/ConfirmablePasswordController.js +14 -0
- package/templates/app/backend/app/Http/Controllers/Auth/EmailVerificationController.js +23 -0
- package/templates/app/backend/app/Http/Controllers/Auth/NewPasswordController.js +29 -0
- package/templates/app/backend/app/Http/Controllers/Auth/PasswordResetLinkController.js +20 -0
- package/templates/app/backend/app/Http/Controllers/Auth/RegisteredUserController.js +19 -0
- package/templates/app/backend/app/Http/Controllers/Auth/SessionController.js +36 -0
- package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorChallengeController.js +41 -0
- package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorController.js +63 -0
- package/templates/app/backend/app/Http/Controllers/ProfileController.js +66 -0
- package/templates/app/backend/app/Http/Kernel.js +8 -0
- package/templates/app/backend/app/Http/Middleware/Authenticate.js +84 -0
- package/templates/app/backend/app/Http/Middleware/CheckAbilities.js +17 -0
- package/templates/app/backend/app/Http/Middleware/CheckForAnyAbility.js +12 -0
- package/templates/app/backend/app/Http/Middleware/EnsureEmailIsVerified.js +11 -0
- package/templates/app/backend/app/Http/Middleware/EnsureHasRole.js +11 -0
- package/templates/app/backend/app/Http/Middleware/EnsureIsAdmin.js +11 -0
- package/templates/app/backend/app/Http/Middleware/RequirePassword.js +15 -0
- package/templates/app/backend/app/Http/Requests/Auth/LoginRequest.js +45 -0
- package/templates/app/backend/app/Http/Resources/.gitkeep +1 -0
- package/templates/app/backend/app/Jobs/.gitkeep +0 -0
- package/templates/app/backend/app/Listeners/.gitkeep +0 -0
- package/templates/app/backend/app/Mail/.gitkeep +0 -0
- package/templates/app/backend/app/Models/PersonalAccessToken.js +21 -0
- package/templates/app/backend/app/Models/User.js +64 -0
- package/templates/app/backend/app/Notifications/.gitkeep +0 -0
- package/templates/app/backend/app/Observers/.gitkeep +0 -0
- package/templates/app/backend/app/Policies/.gitkeep +0 -0
- package/templates/app/backend/app/Providers/AppServiceProvider.js +12 -0
- package/templates/app/backend/app/Rules/.gitkeep +1 -0
- package/templates/app/backend/config/app.js +16 -0
- package/templates/app/backend/config/auth.js +34 -0
- package/templates/app/backend/config/database.js +38 -0
- package/templates/app/backend/config/redis.js +28 -0
- package/templates/app/backend/config/session.js +36 -0
- package/templates/app/backend/database/migrations/0001_01_01_000000_create_users_table.js +49 -0
- package/templates/app/backend/database/migrations/0001_01_01_000001_create_cache_table.js +13 -0
- package/templates/app/backend/database/migrations/0001_01_01_000002_create_jobs_table.js +26 -0
- package/templates/app/backend/database/migrations/0001_01_01_000003_create_notifications_table.js +18 -0
- package/templates/app/backend/database/seeders/DatabaseSeeder.js +15 -0
- package/templates/app/backend/lang/ar/messages.json +10 -0
- package/templates/app/backend/lang/ar/validation.json +21 -0
- package/templates/app/backend/lang/en/messages.json +10 -0
- package/templates/app/backend/lang/en/validation.json +19 -0
- package/templates/app/backend/routes/api.js +25 -0
- package/templates/app/backend/routes/auth.js +52 -0
- package/templates/app/backend/routes/console.js +15 -0
- package/templates/app/frontend/App.kreftjs +4 -0
- package/templates/app/frontend/components/Field.kreftjs +19 -0
- package/templates/app/frontend/components/StatCard.kreftjs +47 -0
- package/templates/app/frontend/components/profile/ApiTokens.kreftjs +70 -0
- package/templates/app/frontend/components/profile/Sessions.kreftjs +42 -0
- package/templates/app/frontend/components/profile/TwoFactor.kreftjs +90 -0
- package/templates/app/frontend/errors/ErrorMonitor.kreftjs +264 -0
- package/templates/app/frontend/index.html +79 -0
- package/templates/app/frontend/kreftjs.d.ts +579 -0
- package/templates/app/frontend/kreftjs.js +5108 -0
- package/templates/app/frontend/layouts/app.kreftjs +718 -0
- package/templates/app/frontend/layouts/docs.kreftjs +37 -0
- package/templates/app/frontend/layouts/guest.kreftjs +18 -0
- package/templates/app/frontend/pages/Dashboard.kreftjs +273 -0
- package/templates/app/frontend/pages/ErrorMonitor.kreftjs +96 -0
- package/templates/app/frontend/pages/Home.kreftjs +617 -0
- package/templates/app/frontend/pages/Profile.kreftjs +104 -0
- package/templates/app/frontend/pages/Users.kreftjs +240 -0
- package/templates/app/frontend/pages/auth/ConfirmPassword.kreftjs +33 -0
- package/templates/app/frontend/pages/auth/ForgotPassword.kreftjs +36 -0
- package/templates/app/frontend/pages/auth/Login.kreftjs +89 -0
- package/templates/app/frontend/pages/auth/Register.kreftjs +48 -0
- package/templates/app/frontend/pages/auth/ResetPassword.kreftjs +45 -0
- package/templates/app/frontend/pages/auth/VerifyEmail.kreftjs +59 -0
- package/templates/app/frontend/routes/web.kreftjs +34 -0
- package/templates/app/frontend/tests/components/Counter.kreftjs +15 -0
- package/templates/app/frontend/tests/pages/About.kreftjs +12 -0
- package/templates/app/frontend/tests/pages/Test.kreftjs +31 -0
- package/templates/app/frontend/ui.css +517 -0
- package/templates/app/lang/ar/auth.json +6 -0
- package/templates/app/lang/ar/messages.json +6 -0
- package/templates/app/lang/ar/pagination.json +5 -0
- package/templates/app/lang/ar/passwords.json +8 -0
- package/templates/app/lang/ar/validation.json +22 -0
- package/templates/app/lang/ar.json +10 -0
- package/templates/app/lang/en/auth.json +6 -0
- package/templates/app/lang/en/messages.json +6 -0
- package/templates/app/lang/en/pagination.json +5 -0
- package/templates/app/lang/en/passwords.json +8 -0
- package/templates/app/lang/en/validation.json +22 -0
- package/templates/app/lang/en.json +10 -0
- package/templates/errors/401.kreftjs +11 -0
- package/templates/errors/402.kreftjs +11 -0
- package/templates/errors/403.kreftjs +11 -0
- package/templates/errors/404.kreftjs +11 -0
- package/templates/errors/419.kreftjs +11 -0
- package/templates/errors/429.kreftjs +11 -0
- package/templates/errors/500.kreftjs +11 -0
- package/templates/errors/503.kreftjs +11 -0
- package/templates/errors/ErrorMonitor.kreftjs +502 -0
- package/templates/errors/error.kreftjs +11 -0
- package/templates/errors/minimal.kreftjs +25 -0
- package/tools/dev-client.js +125 -0
- package/tools/devtools.js +172 -0
- package/tools/vite-plugin-kreftjs.d.ts +52 -0
- 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,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
|
|
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
|