kreftjs 0.0.19-beta → 0.0.20-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 +1 -0
- package/bin/build.js +13 -2
- package/bin/kreftjs.js +1 -0
- package/book/ar/01-getting-started.md +169 -0
- package/book/ar/02-language.md +330 -0
- package/book/ar/03-components-and-state.md +258 -0
- package/book/ar/04-router-data-forms.md +333 -0
- package/book/ar/05-arabic-rtl.md +175 -0
- package/book/ar/06-backend.md +247 -0
- package/book/ar/07-auth-and-security.md +157 -0
- package/book/ar/08-background-work.md +250 -0
- package/book/ar/09-testing-and-deploy.md +234 -0
- package/book/ar/10-electron-desktop.md +258 -0
- package/book/ar/11-advanced-blog.md +370 -0
- package/book/ar/12-advanced-shop.md +426 -0
- package/book/ar/13-advanced-ui.md +440 -0
- package/book/ar/14-advanced-ops.md +167 -0
- package/book/ar/15-appendix.md +81 -0
- package/book/ar/README.md +62 -0
- package/book/kreft-book-ar.docx +0 -0
- package/docs/README.md +2 -2
- package/kreftjs.d.ts +1 -1
- package/kreftjs.js +436 -74
- package/package.json +4 -3
- package/templates/app/frontend/errors/ErrorMonitor.kreft +2 -2
- package/templates/app/frontend/kreftjs.d.ts +1 -1
- package/templates/app/frontend/kreftjs.js +436 -74
- package/templates/app/frontend/pages/Dashboard.kreft +1 -1
- package/templates/errors/ErrorMonitor.kreft +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# الفصل الخامس: العربية والترجمة والاتجاه (RTL)
|
|
2
|
+
|
|
3
|
+
Kreft يترجم النصوص ويقلب الاتجاه للغات التي تُكتب من اليمين إلى اليسار: العربية والعبرية والفارسية والأردية. هذا الفصل مخصص لنا.
|
|
4
|
+
|
|
5
|
+
## ملفات الترجمة
|
|
6
|
+
|
|
7
|
+
المشروع الجديد يأتي بمجلد `lang/` في جذره، وفيه العربية والإنجليزية جاهزتان: `lang/ar.json` و`lang/ar/messages.json` و`lang/ar/validation.json` و`lang/ar/pagination.json` (ويُقرأ أيضًا `frontend/lang/` و`backend/lang/` و`resources/lang/`). استعمل `lang/ar.json` للنصوص البسيطة، أو `lang/ar/messages.json` للمفاتيح المجمّعة.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
// lang/ar/messages.json
|
|
11
|
+
{
|
|
12
|
+
"welcome": "أهلًا بك في تطبيقي",
|
|
13
|
+
"hello": "مرحبًا، :name",
|
|
14
|
+
"items": "{0} لا توجد عناصر|{1} عنصر واحد|{2} عنصران|[3,10] :count عناصر|[11,*] :count عنصرًا"
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
// lang/en/messages.json
|
|
20
|
+
{
|
|
21
|
+
"welcome": "Welcome to my app",
|
|
22
|
+
"hello": "Hello, :name",
|
|
23
|
+
"items": "{0} No items|{1} One item|[2,*] :count items"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
وفي أي صفحة:
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<h1>{$t('messages.hello', { name: user.name })}</h1>
|
|
31
|
+
<p>{$choice('messages.items', count)}</p>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- الرسالة تستعمل `:name` للقيم.
|
|
35
|
+
- للجمع افصل الصيغ بـ `|`، وحدّد المجال بين `{}` للقيمة الواحدة (`{0}` و`{2}`) أو `[من,إلى]` (`[3,10]` و`[11,*]`). هذا يناسب العربية تمامًا: صفر، وواحد، واثنان، وجمع القلة (3–10)، وما فوق (11+) بصيغة التمييز المنصوب.
|
|
36
|
+
|
|
37
|
+
## تبديل اللغة
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
Kreftjs.locale('ar') // يتبدّل <html lang> و dir وتُعاد رسم كل الصفحات
|
|
41
|
+
Kreftjs.fallbackLocale('en') // اللغة الاحتياطية
|
|
42
|
+
Kreftjs.locales() // اللغات المحمّلة
|
|
43
|
+
Kreftjs.translations('ar', { 'app.title': 'تطبيقي' }) // نصوص إضافية من الشيفرة
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## مبدّل لغة كامل (مع التذكّر)
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<!-- components/LangSwitch.kreft -->
|
|
50
|
+
saved lang = 'ar' as 'app.lang'
|
|
51
|
+
|
|
52
|
+
watch lang { Kreftjs.locale(value) }
|
|
53
|
+
on start { Kreftjs.locale(lang) }
|
|
54
|
+
|
|
55
|
+
<view>
|
|
56
|
+
<button class="lang" @click="lang = lang === 'ar' ? 'en' : 'ar'">
|
|
57
|
+
{lang === 'ar' ? 'English' : 'العربية'}
|
|
58
|
+
</button>
|
|
59
|
+
</view>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `saved` يتذكر الاختيار بعد إعادة التحميل.
|
|
63
|
+
- `watch` يطبّق اللغة عند كل تغيير.
|
|
64
|
+
- `on start` يطبّقها عند فتح الصفحة.
|
|
65
|
+
|
|
66
|
+
## الاتجاه
|
|
67
|
+
|
|
68
|
+
يُضبط `dir` تلقائيًا من اللغة، ويمكنك ربطه بيدك:
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<view dir={isArabic ? 'rtl' : 'ltr'}>…</view>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## اكتب CSS ينقلب وحده
|
|
75
|
+
|
|
76
|
+
استعمل الخصائص المنطقية (logical properties) بدل left/right:
|
|
77
|
+
|
|
78
|
+
| تجنّب | استعمل |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `margin-left` / `margin-right` | `margin-inline-start` / `margin-inline-end` |
|
|
81
|
+
| `padding-left` / `padding-right` | `padding-inline-start` / `padding-inline-end` |
|
|
82
|
+
| `left: 0` / `right: 0` | `inset-inline-start: 0` / `inset-inline-end: 0` |
|
|
83
|
+
| `text-align: left` / `right` | `text-align: start` / `end` |
|
|
84
|
+
| `border-left` / `border-right` | `border-inline-start` / `border-inline-end` |
|
|
85
|
+
|
|
86
|
+
```css
|
|
87
|
+
.sidebar {
|
|
88
|
+
position: fixed;
|
|
89
|
+
inset-inline-start: 0; /* يسار في LTR، يمين في RTL */
|
|
90
|
+
padding-inline-start: 24px;
|
|
91
|
+
border-inline-end: 1px solid var(--border);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[dir="rtl"] .back-arrow { transform: scaleX(-1); } /* اقلب أيقونة */
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## نصائح عملية للتطبيقات العربية
|
|
98
|
+
|
|
99
|
+
1. **الأرقام:** استعمل `Intl.NumberFormat('ar-EG')` للأرقام الهندية أو `ar-SA`/`ar-MA` لما تفضله بلدك:
|
|
100
|
+
```html
|
|
101
|
+
derive fmt = new Intl.NumberFormat(lang === 'ar' ? 'ar-EG' : 'en-US')
|
|
102
|
+
<p>{fmt.format(total)}</p>
|
|
103
|
+
```
|
|
104
|
+
2. **التواريخ:** `new Date().toLocaleDateString('ar', { dateStyle: 'long' })`. اختر التقويم بـ `ar-SA-u-ca-islamic` إذا أردت الهجري.
|
|
105
|
+
3. **الخطوط:** اختر خطًا عربيًا (مثل Cairo أو Tajawal). في تطبيقات سطح المكتب لا تعتمد على تحميل خط من الإنترنت؛ ضمّن الخط محليًا.
|
|
106
|
+
4. **الأيقونات الاتجاهية:** الأسهم والرجوع تحتاج `scaleX(-1)` في RTL كما في المثال.
|
|
107
|
+
5. **النص المختلط:** اللاتينية داخل العربية قد تبدّل ترتيب علامات الترقيم. غلّف المقطع بـ `<bdi>` أو `unicode-bidi: isolate`.
|
|
108
|
+
6. **حقول الإدخال:** للبريد والرقم السري وأرقام الهاتف أضف `dir="ltr"` حتى تُكتب صحيحة.
|
|
109
|
+
|
|
110
|
+
```html
|
|
111
|
+
<input type="email" dir="ltr" k-model="form.email" placeholder="name@example.com">
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## على الخادم
|
|
115
|
+
|
|
116
|
+
لغة الطلب تأتي من `?lang=` أو كوكي `lang` أو `Accept-Language`، ورسائل الخادم تُقرأ من `lang/<locale>/`، والعربية جاهزة في المشروع الجديد:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
// lang/ar/validation.json (موجود أصلًا في المشروع الجديد)
|
|
120
|
+
{
|
|
121
|
+
"required": "حقل :attribute مطلوب.",
|
|
122
|
+
"email": "يجب أن يكون :attribute عنوان بريد إلكتروني صالح.",
|
|
123
|
+
"min": { "string": "يجب أن يحتوي :attribute على :min أحرف على الأقل." },
|
|
124
|
+
"attributes": { "email": "البريد الإلكتروني", "password": "كلمة المرور" },
|
|
125
|
+
"not_common_password": "كلمة المرور شائعة جدًا أو سهلة التخمين. اختر غيرها."
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
المفتاح `attributes` يترجم أسماء الحقول داخل الرسائل: فتظهر "حقل البريد الإلكتروني مطلوب" بدل "حقل email مطلوب".
|
|
130
|
+
|
|
131
|
+
وللتخصيص الدقيق لنموذج بعينه استعمل `messages()` في `FormRequest`.
|
|
132
|
+
|
|
133
|
+
## مثال: صفحة هبوط ثنائية اللغة مع SEO
|
|
134
|
+
|
|
135
|
+
```html
|
|
136
|
+
<!-- pages/Home.kreft -->
|
|
137
|
+
import LangSwitch from '../components/LangSwitch.kreft'
|
|
138
|
+
|
|
139
|
+
state count = 3
|
|
140
|
+
|
|
141
|
+
<view>
|
|
142
|
+
<header class="top">
|
|
143
|
+
<h1>{$t('messages.hello', { name: 'الزائر' })}</h1>
|
|
144
|
+
<lang-switch></lang-switch>
|
|
145
|
+
</header>
|
|
146
|
+
<p>{$choice('messages.items', count)}</p>
|
|
147
|
+
<button @click="count++">+</button>
|
|
148
|
+
</view>
|
|
149
|
+
|
|
150
|
+
<style scoped>
|
|
151
|
+
.top {
|
|
152
|
+
display: flex;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
padding-inline: 24px;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
ولظهور الصفحة صحيحة لمحركات البحث بلغتين، أعلن البدائل في `backend/routes/seo.js` (انظر فصل النشر):
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
module.exports = {
|
|
163
|
+
'/': {
|
|
164
|
+
title: 'متجري',
|
|
165
|
+
description: 'كل ما يحتاجه بيتك',
|
|
166
|
+
alternates: { en: 'https://example.com/', ar: 'https://example.com/ar/' },
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## تمارين
|
|
172
|
+
|
|
173
|
+
1. اجعل تطبيقك ثنائي اللغة (عربي/إنجليزي) واحفظ اللغة المختارة.
|
|
174
|
+
2. حوّل كل `margin-left` و`padding-right` في CSS تطبيقك إلى خصائص منطقية وجرّب التبديل.
|
|
175
|
+
3. اكتب مكوّن سعر يعرض المبلغ بالأرقام الهندية عند العربية.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# الفصل السادس: الواجهة الخلفية
|
|
2
|
+
|
|
3
|
+
المشروع الذي ينشئه `kreft new` فيه واجهة خلفية على Node.js. الأمران `kreft dev` و`kreft serve` يحمّلان `backend/routes/api.js` بنفسيهما، وكل عنوان فيه يبدأ بـ `/api`، و`kreft dev` يلتقط كل تغيير تحفظه. مجلد `backend` لا يصل المتصفح أبدًا.
|
|
4
|
+
|
|
5
|
+
## المسارات (Routes)
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// backend/routes/api.js
|
|
9
|
+
const { Route } = require('kreftjs/server')
|
|
10
|
+
const PostController = require('../app/Http/Controllers/PostController')
|
|
11
|
+
|
|
12
|
+
Route.get('/hello', (req) => ({ hello: req.input('name', 'world') })) // GET /api/hello
|
|
13
|
+
|
|
14
|
+
Route.apiResource('posts', PostController) // index, store, show, update, destroy
|
|
15
|
+
Route.get('/posts/{post}/comments', [PostController, 'comments']).name('posts.comments')
|
|
16
|
+
|
|
17
|
+
Route.middleware('auth').group(() => {
|
|
18
|
+
Route.delete('/posts/{post}', [PostController, 'destroy'])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
Route.prefix('admin').middleware(['auth', 'admin']).group(() => { /* … */ })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- المعاملات تُكتب `{post}`، أو `{post?}` إن كانت اختيارية.
|
|
25
|
+
- للمسار اسم (`.name('…')`) ونمط (`.whereNumber('id')`) ووسيط (`.middleware('auth')`).
|
|
26
|
+
- `kreft route:list` يطبع كل المسارات مع أسمائها ومتحكماتها ووسطائها.
|
|
27
|
+
|
|
28
|
+
### فضاءات الأسماء (Namespaces)
|
|
29
|
+
|
|
30
|
+
فضاءات الأسماء بأسلوب PHP مدعومة للمتحكمات والنماذج والعلاقات:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const { Route, use, namespace } = require('kreftjs/server')
|
|
34
|
+
|
|
35
|
+
const User = use('App\\Models\\User')
|
|
36
|
+
const { DashboardController, UserController } = namespace('App\\Http\\Controllers\\Admin')
|
|
37
|
+
|
|
38
|
+
Route.namespace('Admin').group(() => {
|
|
39
|
+
Route.get('/users', 'UserController@index')
|
|
40
|
+
Route.apiResource('users', 'UserController')
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## المتحكمات (Controllers) والطلب
|
|
45
|
+
|
|
46
|
+
دالة المتحكم تستلم الطلب أولًا، ثم معاملات المسار. ما تعيده يُرسل كـ JSON، وإن لم تعد شيئًا فالجواب `204`.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const { response, abort } = require('kreftjs/server')
|
|
50
|
+
const Post = require('../../Models/Post')
|
|
51
|
+
|
|
52
|
+
class PostController {
|
|
53
|
+
async index(req) {
|
|
54
|
+
return Post.with('author').where('published', true).latest().paginate(15, req.integer('page', 1))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async store(req) {
|
|
58
|
+
const data = await req.validate({
|
|
59
|
+
title: 'required|max:200|unique:posts,title',
|
|
60
|
+
author_id: 'required|exists:authors,id',
|
|
61
|
+
})
|
|
62
|
+
return response(await Post.create(data), 201)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async show(req, id) { return Post.findOrFail(id) } // 404 إن لم يوجد
|
|
66
|
+
async destroy(req, id) { await (await Post.findOrFail(id)).delete() }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = PostController
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
ولتوليد متحكم: `kreft make:controller PostController --api` (وأيضًا `--model Post` و`--route=api` و`--auth`).
|
|
73
|
+
|
|
74
|
+
### الطلب (req)
|
|
75
|
+
|
|
76
|
+
| الاستدعاء | يعطي |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `req.input('name', default)` و`req.all()` و`req.only('a', 'b')` و`req.except(…)` و`req.has(…)` و`req.filled(…)` | قيمًا من الجسم والاستعلام. |
|
|
79
|
+
| `req.boolean('x')` و`req.integer('page', 1)` و`req.string('q')` | قيمة بالنوع الصحيح. |
|
|
80
|
+
| `await req.validate(rules)` | القواعد نفسها كما في `form()`، إضافة إلى `unique:table,column` و`exists:table,column`. يعيد الحقول المفحوصة فقط أو يجيب `422`. |
|
|
81
|
+
| `req.params` و`req.query` و`req.body` و`req.header('x')` و`req.bearerToken()` و`req.ip()` و`req.user` | الباقي. |
|
|
82
|
+
| `req.file('avatar').store('avatars')` | يحفظ رفعًا باسم عشوائي. |
|
|
83
|
+
| `response(data, 201)` و`response(data).header('X', 'y')` و`abort(403, 'message')` | الأجوبة. |
|
|
84
|
+
|
|
85
|
+
### طلبات النماذج والموارد والسياسات
|
|
86
|
+
|
|
87
|
+
للنقاط الأكبر، انقل القواعد والتفويض خارج المتحكم:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
// app/Http/Requests/StorePostRequest.js
|
|
91
|
+
const { FormRequest } = require('kreftjs/server')
|
|
92
|
+
|
|
93
|
+
class StorePostRequest extends FormRequest {
|
|
94
|
+
authorize(req) { return req.user != null } // false تجيب 403
|
|
95
|
+
rules() { return { title: 'required|max:200', body: 'required' } }
|
|
96
|
+
messages() { return { 'title.required': 'أعطِ المقال عنوانًا.' } }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = StorePostRequest
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const data = await StorePostRequest.validate(req) // في متحكم
|
|
104
|
+
return PostResource.collection(await Post.paginate(15)) // { data, meta }
|
|
105
|
+
await req.authorize('update', post) // app/Policies/PostPolicy.js
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
ولتوليدها: `kreft make:request` و`kreft make:resource` و`kreft make:policy`.
|
|
109
|
+
|
|
110
|
+
## النماذج (Models) وقاعدة البيانات
|
|
111
|
+
|
|
112
|
+
النماذج أصناف Active Record، كما في Eloquent:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
// app/Models/Post.js
|
|
116
|
+
const { Model, Str } = require('kreftjs/server')
|
|
117
|
+
|
|
118
|
+
class Post extends Model {
|
|
119
|
+
static fillable = ['title', 'body', 'user_id'] // ما يجوز لـ create() و update() ملؤه
|
|
120
|
+
static hidden = [] // لا يُرسل كـ JSON أبدًا
|
|
121
|
+
static casts = { published: 'boolean', meta: 'json' }
|
|
122
|
+
static softDeletes = true // delete() يملأ deleted_at
|
|
123
|
+
|
|
124
|
+
user() { return this.belongsTo(require('./User')) }
|
|
125
|
+
comments() { return this.hasMany(require('./Comment')) }
|
|
126
|
+
tags() { return this.belongsToMany(require('./Tag')) }
|
|
127
|
+
|
|
128
|
+
static scopePublished(q) { q.where('published', true) }
|
|
129
|
+
static async creating(post) { post.slug ??= Str.slug(post.title) }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = Post
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
الجدول هو جمع اسم الصنف (`Post` ← `posts`)؛ استعمل `static table = '…'` لجدول آخر.
|
|
136
|
+
|
|
137
|
+
### الاستعلام
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
await Post.all()
|
|
141
|
+
await Post.find(5) // أو findOrFail(5): 404 إن لم يوجد
|
|
142
|
+
await Post.where('published', true).orderByDesc('id').limit(10).get()
|
|
143
|
+
await Post.where('views', '>', 100).orWhere('pinned', true).count()
|
|
144
|
+
await Post.query().published().with('user', 'comments.author').withCount('comments').paginate(15, page)
|
|
145
|
+
await Post.query().whereHas('comments', (q) => q.where('approved', true)).get()
|
|
146
|
+
|
|
147
|
+
const post = await Post.create({ title: 'مرحبًا' }) // يملأ created_at و updated_at
|
|
148
|
+
await post.update({ title: 'أهلًا' })
|
|
149
|
+
await post.delete()
|
|
150
|
+
|
|
151
|
+
await post.comments().create({ body: 'جميل' })
|
|
152
|
+
await post.tags().sync([1, 2, 3]) // وأيضًا attach() و detach() و toggle()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
العلاقات: `hasOne` و`hasMany` و`belongsTo` و`belongsToMany` (مع `pivot`) و`hasManyThrough`. الصفوف المحذوفة حذفًا ناعمًا تعود بـ `withTrashed()` وتُسترجع بـ `restore()`.
|
|
156
|
+
|
|
157
|
+
بلا نموذج:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
await DB.table('orders').join('users', 'users.id', '=', 'orders.user_id').groupBy('status').get()
|
|
161
|
+
await DB.select('SELECT * FROM posts WHERE id = ?', [5])
|
|
162
|
+
await DB.transaction(async () => { await Order.create({ /* … */ }); await Stock.decrement(/* … */) }) // الكل أو لا شيء
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
> **تحذير:** القيم تسافر دائمًا كمعاملات (`?`). لا تبنِ SQL بضمّ مدخلات المستخدم في نص. أما `whereRaw` و`selectRaw` و`orderByRaw` فتأخذ SQL خامًا عمدًا: مرّر قيم المستخدم عبر مصفوفة المعاملات فيها.
|
|
166
|
+
|
|
167
|
+
### قواعد البيانات
|
|
168
|
+
|
|
169
|
+
تأتي القاعدة من `DATABASE_URL` في `.env`:
|
|
170
|
+
|
|
171
|
+
```env
|
|
172
|
+
DATABASE_URL=sqlite:backend/database/app.db # مدمجة في Node.js، لا شيء لتثبيته
|
|
173
|
+
DATABASE_URL=mysql://user:pass@localhost:3306/shop # npm i mysql2
|
|
174
|
+
DATABASE_URL=postgres://user:pass@localhost:5432/shop # npm i pg
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
الترحيلات والنماذج نفسها تعمل على الثلاث.
|
|
178
|
+
|
|
179
|
+
## الترحيلات (Migrations) والبذور (Seeders) والمصانع (Factories)
|
|
180
|
+
|
|
181
|
+
الترحيل يصف جدولًا بـ JavaScript، والملف نفسه يعمل على SQLite وMySQL وPostgreSQL:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
// database/migrations/2026_10_29_120000_create_posts_table.js
|
|
185
|
+
module.exports = {
|
|
186
|
+
up(schema) {
|
|
187
|
+
schema.create('posts', (table) => {
|
|
188
|
+
table.id()
|
|
189
|
+
table.foreignId('author_id').constrained().cascadeOnDelete()
|
|
190
|
+
table.string('title', 200).unique()
|
|
191
|
+
table.text('body').nullable()
|
|
192
|
+
table.boolean('published').default(false)
|
|
193
|
+
table.json('meta').nullable()
|
|
194
|
+
table.timestamps()
|
|
195
|
+
})
|
|
196
|
+
},
|
|
197
|
+
down(schema) {
|
|
198
|
+
schema.dropIfExists('posts')
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
أنواع الأعمدة: `id` و`string` و`text` و`integer` و`bigInteger` و`decimal` و`float` و`boolean` و`date` و`dateTime` و`timestamp` و`timestamps` و`json` و`foreignId`. وملفات `.sql` العادية في `database/migrations` تعمل أيضًا (تُنفَّذ مرة لكل ملف بترتيب الأسماء).
|
|
204
|
+
|
|
205
|
+
البذرة تملأ بيانات البداية:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
// database/seeders/DatabaseSeeder.js
|
|
209
|
+
const { Seeder } = require('kreftjs/server')
|
|
210
|
+
const Post = require('../../app/Models/Post')
|
|
211
|
+
|
|
212
|
+
class DatabaseSeeder extends Seeder {
|
|
213
|
+
async run() {
|
|
214
|
+
await Post.create({ title: 'مرحبًا', author_id: 1 })
|
|
215
|
+
await this.call(require('./CommentSeeder'))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = DatabaseSeeder
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
المصانع تصنع بيانات وهمية للبذور والاختبارات:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
await User.factory().count(10).has(Post.factory().count(3), 'posts').create()
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### الأوامر
|
|
229
|
+
|
|
230
|
+
| الأمر | ماذا يفعل |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `kreft migrate` | ينفّذ الترحيلات الجديدة (`kreft dev` يفعل ذلك وحده). |
|
|
233
|
+
| `kreft migrate:rollback` | يتراجع عن آخر دفعة (`--step 2` لآخر اثنتين). |
|
|
234
|
+
| `kreft migrate:fresh --seed` | يحذف كل الجداول ثم يرحّل من جديد ثم يبذر. |
|
|
235
|
+
| `kreft migrate:status` | أي الترحيلات نُفّذت. |
|
|
236
|
+
| `kreft db:seed` | ينفّذ `DatabaseSeeder` (`--class PostSeeder` لغيره). |
|
|
237
|
+
| `kreft make:model Post -mfsc` | نموذج مع ترحيل (`-m`) ومصنع (`-f`) وبذرة (`-s`) ومتحكم (`-c`) وصفحة (`-p`) ومسار (`-r`). |
|
|
238
|
+
| `kreft make:migration create_posts_table` | `add_x_to_posts_table` يصنع `schema.table`. |
|
|
239
|
+
| `kreft make:seeder` و`kreft make:middleware` | الملفات في أماكنها الصحيحة. |
|
|
240
|
+
|
|
241
|
+
> **ملاحظة:** `migrate:fresh` و`db:wipe` يدمّران البيانات. هما لقواعد التطوير فقط.
|
|
242
|
+
|
|
243
|
+
## تمارين
|
|
244
|
+
|
|
245
|
+
1. أنشئ مشروعًا بـ `--db sqlite`، ثم `kreft make:model Product -mfsc` وعدّل الترحيل بإضافة `price` و`stock`.
|
|
246
|
+
2. أضف مسار `GET /api/products/low-stock` يعيد المنتجات التي مخزونها أقل من 5 باستعمال scope.
|
|
247
|
+
3. اكتب بذرة تنشئ 50 منتجًا بالمصنع، وشغّل `kreft migrate:fresh --seed`.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# الفصل السابع: المصادقة والوسطاء والأمان
|
|
2
|
+
|
|
3
|
+
## صفحات الدخول الجاهزة
|
|
4
|
+
|
|
5
|
+
المشاريع الجديدة (بلا `--no-auth`) تأتي بكل شاشات الدخول والواجهة الخلفية التي وراءها:
|
|
6
|
+
|
|
7
|
+
| الصفحة | العنوان | ماذا تفعل |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `pages/auth/Login.kreft` | `/login` | الدخول بالبريد وكلمة المرور. |
|
|
10
|
+
| `pages/auth/Register.kreft` | `/register` | إنشاء حساب مع بريد لتأكيد العنوان. |
|
|
11
|
+
| `pages/auth/ForgotPassword.kreft` | `/forgot-password` | يرسل رابطًا لاختيار كلمة مرور جديدة. |
|
|
12
|
+
| `pages/auth/ResetPassword.kreft` | `/reset-password/:token` | الرابط القادم في ذلك البريد. |
|
|
13
|
+
| `pages/auth/VerifyEmail.kreft` | `/verify-email` | "تحقق من بريدك"، ورابطه يفتح `/verify-email/:id/:hash`. |
|
|
14
|
+
| `pages/auth/ConfirmPassword.kreft` | `/confirm-password` | أعد كتابة كلمة المرور قبل أمر خطير. |
|
|
15
|
+
| `pages/Dashboard.kreft` و`pages/Profile.kreft` | `/dashboard` و`/profile` | أول صفحة بعد الدخول؛ إعدادات الحساب. |
|
|
16
|
+
|
|
17
|
+
## كيف يعمل الدخول
|
|
18
|
+
|
|
19
|
+
- الدخول يضع رمزًا في **كوكي HttpOnly**. سكربتات الصفحة لا تقرؤه، فلا يسرقه سكربت محقون في الصفحة. وفي قاعدة البيانات يُخزَّن تجزئة الرمز فقط.
|
|
20
|
+
- كل تغيير (POST وPUT وPATCH وDELETE) يحتاج ترويسة `X-XSRF-TOKEN` مطابقة لكوكي `XSRF-TOKEN`. و`Kreftjs.api` يفعل ذلك عنك.
|
|
21
|
+
- أي سكربت أو تطبيق يرسل `device_name` عند الدخول يحصل على **رمز API** (`Authorization: Bearer 5|…`) بدل الكوكي.
|
|
22
|
+
- كلمات المرور تُجزَّأ بـ scrypt. روابط الاستعادة تنتهي بعد 60 دقيقة وتُخرج المستخدم من كل مكان.
|
|
23
|
+
- معرّف الجلسة يتغير عند كل دخول، فلا يمكن زرع جلسة في متصفح مسبقًا.
|
|
24
|
+
|
|
25
|
+
على الخادم:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
Route.middleware(['auth', 'verified']).group(() => {
|
|
29
|
+
Route.get('/orders', [OrderController, 'index']) // req.user هو المستخدم
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
Route.delete('/account', /* … */).middleware(['auth', 'password.confirm']) // يطلب كلمة المرور مجددًا (423)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
مساعدات تستعملها في أي مكان بالواجهة الخلفية:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const { Hash, Str, Url, Mail, Crypt } = require('kreftjs/server')
|
|
39
|
+
|
|
40
|
+
Hash.make('secret') // خزّن هذه، لا كلمة المرور أبدًا
|
|
41
|
+
Hash.check('secret', user.password) // true أو false
|
|
42
|
+
Str.random(40) // رمز عشوائي
|
|
43
|
+
Url.signed('/verify-email/5/abc', 60) // رابط يعمل 60 دقيقة دون تغيير
|
|
44
|
+
await Mail.send({ to, subject, text }) // يُطبع في الطرفية حتى تضبط MAIL_URL
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## حمايات في مسارات الدخول
|
|
48
|
+
|
|
49
|
+
- الزوار لهم `throttle:6,1`: ست محاولات في الدقيقة لكل عنوان.
|
|
50
|
+
- كل **حساب** يُقفَل أيضًا بعد `LOGIN_MAX_FAILURES` (10) كلمات مرور خاطئة في `LOGIN_WINDOW_SECONDS` (15 دقيقة)، أيًّا كان عنوان المحاولات. بعدها حتى الكلمة الصحيحة تعطي `429` حتى تنتهي النافذة.
|
|
51
|
+
- رموز المصادقة الثنائية تُعدّ لكل حساب أيضًا: `TWO_FACTOR_MAX_FAILURES` (8) رمزًا خاطئًا يقفل الحساب، وبدء دخول جديد لا يصفّر العدّ.
|
|
52
|
+
- "نسيت كلمة المرور؟" تعطي الجواب نفسه سواء كان للعنوان حساب أو لا.
|
|
53
|
+
- تغيير البريد يحتاج تأكيد كلمة المرور في آخر 3 ساعات (الجواب `423`)، لأن العنوان هو ما تثق به استعادة كلمة المرور.
|
|
54
|
+
|
|
55
|
+
## المصادقة الثنائية والأجهزة ورموز API
|
|
56
|
+
|
|
57
|
+
- **المصادقة الثنائية:** رموز تطبيق المصادقة (TOTP) ورموز استرداد. كل رمز استرداد يعمل مرة واحدة.
|
|
58
|
+
- **الأجهزة:** `/profile` يعرض المتصفحات المسجّلة، ويمكن إخراج أي منها.
|
|
59
|
+
- **رموز API:** تُنشأ في `/profile` بالصلاحيات `read` و`create` و`update` و`delete`. الرمز لا يستعمل إلا طرق HTTP التي تسمح بها صلاحياته، على **كل** مسار (`GET` تحتاج `read`، و`POST` تحتاج `create`، و`PUT`/`PATCH` تحتاج `update`، و`DELETE` تحتاج `delete`). جلسات المتصفح ورموز `*` فقط تدير الرموز وكلمة المرور والحساب.
|
|
60
|
+
|
|
61
|
+
### المديرون
|
|
62
|
+
|
|
63
|
+
المدير مستخدم عموده `is_admin` صحيح. المستخدم التجريبي مدير؛ وامنح غيره الصلاحية من صفحة المستخدمين أو بـ `kreft make:admin you@example.com`. واستعمل `Route.middleware(['auth', 'admin'])` لمسارات المديرين.
|
|
64
|
+
|
|
65
|
+
## الوسطاء (Middleware)
|
|
66
|
+
|
|
67
|
+
الوسيط يعمل قبل المتحكم، ويمكنه إيقاف الطلب أو تغيير الجواب:
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// app/Http/Middleware/EnsureIsAdmin.js ('ensure-is-admin' في مسار، أو 'ensure-is-admin:editor')
|
|
71
|
+
const { abort } = require('kreftjs/server')
|
|
72
|
+
|
|
73
|
+
class EnsureIsAdmin {
|
|
74
|
+
async handle(req, next, role = 'admin') {
|
|
75
|
+
if (!req.user || req.user.role !== role) abort(403)
|
|
76
|
+
const response = await next(req) // المتحكم يعمل هنا
|
|
77
|
+
return response.header('X-Checked', 'yes')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = EnsureIsAdmin
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
الاسم في المسار يجد ملفه في `app/Http/Middleware`: `'auth'` هو `Authenticate.js`، و`'ensure-is-admin'` هو `EnsureIsAdmin.js`.
|
|
85
|
+
|
|
86
|
+
### الوسطاء المدمجة
|
|
87
|
+
|
|
88
|
+
| الوسيط | ماذا يفعل |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `throttle:60,1` | تحديد المعدل: 60 طلبًا في الدقيقة لكل زائر ومسار، ثم `429`. |
|
|
91
|
+
| `csrf` | يفحص رمز CSRF على POST وPUT وPATCH وDELETE (`419` عند الخطأ). |
|
|
92
|
+
| `can:ability,model` | فحوص السياسات عبر `Gate.authorize()`. |
|
|
93
|
+
| `auth` و`guest` و`verified` و`password.confirm` | مجموعة الدخول من `app/Http/Middleware`. |
|
|
94
|
+
|
|
95
|
+
ولكل المسارات اذكر الوسطاء في `app/Http/Kernel.js`:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
module.exports = {
|
|
99
|
+
middleware: ['throttle:120,1'],
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
استثنِ مسارًا بـ `.withoutMiddleware('csrf')`، أو `new VerifyCsrfToken({ except: ['webhooks/*'] })`.
|
|
104
|
+
|
|
105
|
+
## السياسات (Policies) والتفويض
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
// app/Policies/PostPolicy.js
|
|
109
|
+
class PostPolicy {
|
|
110
|
+
update(user, post) { return user.id === post.user_id }
|
|
111
|
+
delete(user, post) { return user.is_admin || user.id === post.user_id }
|
|
112
|
+
}
|
|
113
|
+
module.exports = PostPolicy
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
// في متحكم
|
|
118
|
+
async update(req, id) {
|
|
119
|
+
const post = await Post.findOrFail(id)
|
|
120
|
+
await req.authorize('update', post) // 403 إن رفضت السياسة
|
|
121
|
+
await post.update(await UpdatePostRequest.validate(req))
|
|
122
|
+
return post
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
وللتوليد: `kreft make:policy PostPolicy`. وكتابة `can:update,post` في الوسيط تفعل الشيء نفسه على مستوى المسار.
|
|
127
|
+
|
|
128
|
+
## قائمة فحص قبل الإنتاج
|
|
129
|
+
|
|
130
|
+
| الفحص | لماذا |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `NODE_ENV=production` و`APP_ENV=production` | يطفئ صفحات التصحيح ويفعّل HSTS وتحويل https ويجعل أخطاء الأمان قاتلة. |
|
|
133
|
+
| `APP_DEBUG=false` | لا تُظهر آثار المكدّس أو الشيفرة للزوار أبدًا. |
|
|
134
|
+
| `APP_KEY` بمفتاح من 32 بايت (`kreft key:generate`) | في الإنتاج يرفض التطبيق البدء بمفتاح أقل من 16 بايت. |
|
|
135
|
+
| `APP_URL=https://your.site` | تُستعمل لروابط البريد وتحويل https والكوكيز الآمنة. |
|
|
136
|
+
| `TRUSTED_PROXIES` إن استعملت وسيطًا | وإلا يشترك كل الزوار في عنوان الوسيط وحدود المعدل. |
|
|
137
|
+
| `DATABASE_URL` حقيقي وخطة نسخ احتياطي | SQLite تكفي للصغير؛ استعمل MySQL أو PostgreSQL لعدة خوادم. |
|
|
138
|
+
| `CACHE_STORE=redis` أو `database`، و`QUEUE_CONNECTION=redis` أو `database` | `memory` لكل عملية على حدة. وأقفال الحسابات في الذاكرة المؤقتة. |
|
|
139
|
+
| `MAIL_URL` مضبوط | وإلا تُطبع الرسائل في الطرفية فقط. |
|
|
140
|
+
| `MCP_TOKEN` مضبوط، أو MCP مطفأ | نقطة MCP معطلة في الإنتاج بدونه. |
|
|
141
|
+
| لا مستخدم تجريبي | لا تشغّل `--seed` على الإنتاج؛ واحذف `test@example.com`. |
|
|
142
|
+
| HTTPS أمام التطبيق مع `FORCE_HTTPS=true` | الكوكيز `Secure` في الإنتاج. |
|
|
143
|
+
| `npm audit` و`kreft update` | أصلح الثغرات المعروفة قبل الإصدار. |
|
|
144
|
+
|
|
145
|
+
> **ملاحظة:** Kreft لا يغني عن مراجعة أمنية. للأنظمة الحساسة، ادعُ متخصصًا لمراجعة التطبيق والبنية والاعتماديات.
|
|
146
|
+
|
|
147
|
+
## أدوات الحماية في الواجهة
|
|
148
|
+
|
|
149
|
+
- القيم تُهرَّب دائمًا في القوالب. و`k-html` يُنظَّف من السكربتات لكن اربط HTML تثق به فقط.
|
|
150
|
+
- التطبيق المبني (`kreft build`) لا يستعمل `eval` ولا `new Function`، فتعمل معه سياسة أمان المحتوى (CSP) الصارمة.
|
|
151
|
+
- `Kreftjs.env` يحمل المتغيرات التي تبدأ بـ `PUBLIC_` فقط؛ لا تضع سرًّا في واحد منها.
|
|
152
|
+
|
|
153
|
+
## تمارين
|
|
154
|
+
|
|
155
|
+
1. أنشئ سياسة `ProductPolicy` تسمح للمدير فقط بالحذف، واختبرها برمز API محدود الصلاحيات.
|
|
156
|
+
2. أضف وسيطًا `ensure-subscribed` يجيب `402` لمن ليس مشتركًا.
|
|
157
|
+
3. فعّل المصادقة الثنائية لحسابك من `/profile` واحتفظ برموز الاسترداد.
|