-
المساهمات
3 -
تاريخ الانضمام
-
تاريخ آخر زيارة
آخر الزوار
لوحة آخر الزوار معطلة ولن تظهر للأعضاء
إنجازات Abdulaziz Hamzah

عضو مبتدئ (1/3)
1
السمعة بالموقع
-
سنشرح في هذا المقال كيفية بناء محاكي مرئي موسيقى باستخدام Three.js والتعامل مع تقنيات مزامنة الصوت مؤثرات مرئية ثلاثية الأبعاد. ما سنحاول بناءه في هذا المقال هو صور متفاعلة ديناميكيًا مع الصوت بشكل قريب من الفكرة الواردة في هاذا المشروع. تهيئة المشروع في البداية سننشئ مشروع جديد بأي محرر أكواد نفضله، ونضيف له مكتبة three.js، ثم ننشئ ملف جافا سكريبت ونضع فيه الكود التالي الذي يضمن أن الصوت سيعمل فقط عندما يطلب المستخدم ذلك وينقر في الصفحة، وذلك بهدف تجنب مشكلات تشغيل الصوت التلقائي التي وضعتها المتصفحات الأساسية وضمان تجربة مستخدم أفضل. export default class App { constructor() { this.onClickBinder = () => this.init() document.addEventListener('click', this.onClickBinder) } init() { document.removeEventListener('click', this.onClickBinder) مشهد Three.js بسيط// this.renderer = new THREE.WebGLRenderer() this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 10000) this.scene = new THREE.Scene() } } تحليل البيانات الصوتية سنهيئ الآن مدير الصوت (Audio Manager) ومدير الإيقاع (BPM Manager) لتحميل الصوت، وتحليله، ومزامنته مع العناصر المرئية. وسنبدأ تشغيلهما باستخدام async و async. async createManagers() { App.audioManager = new AudioManager() await App.audioManager.loadAudioBuffer() App.bpmManager = new BPMManager() App.bpmManager.addEventListener('beat', () => { this.particles.onBPMBeat() }) await App.bpmManager.detectBPM(App.audioManager.audio.buffer) } ملاحظة: يشير عدد النبضات في الدقيقة Beats Per Minute -أو BPM اختصارًا- إلى سرعة الإيقاع الموسيقي. سيحمّل الصنف AudioManager في الكود السابق الصوت من عنوان URL محدد، وفي هذا المقال اخترنا ملف صوتي من موقع Spotify، بعد تحميل الصوت سنحلله ثم نقسّم إشارات الصوت إلى نطاقات ترددية في الوقت الفعلي. const audioLoader = new THREE.AudioLoader(); audioLoader.load(this.song.url, buffer => { this.audio.setBuffer(buffer); }) بيانات التردد علينا فصل نطاق الترددات إلى الترددات المنخفضة والمتوسطة والعالية لحساب السعة amplitude. سنقسم نطاقات الترددات إلى frequency bands كما يلي: ترددات منخفضة Low ترددات متوسطة Mid ترددات عالية High كل مجموعة لها نطاق ترددات محدد، مثلًا تبدأ الترددات المنخفضة من lowFrequency وتنتهي عند بداية midFrequency. وتبدأ الترددات العالية من midFrequency وتنتهي عند highFrequency. ولحساب متوسط السعة داخل نطاق معيّن يمكننا ضرب الترددات بالقيمة Buffer length والتيتمثل عدد العينات الصوتية المخزنة في Buffer، ثم نقسم الناتج على معدل أخذ العينات sample rate، وهو عدد العيّنات التي يتم التقاطها في الثانية فهذا يساعدنا على تحديد الموضع الصحيح داخل المصفوفة التي تحتوي على بيانات الترددات الصوتية بناءً على النطاق الترددي. بعد حساب المتوسط، نحول القيمة الناتجة إلى نطاق يتراوح بين 0 و 1، حتى نتمكن من استخدامها بشكل مرن في التطبيقات الرسومية أو الصوتية. this.lowFrequency = 10; this.frequencyArray = this.audioAnalyser.getFrequencyData(); const lowFreqRangeStart = Math.floor((this.lowFrequency * this.bufferLength) / this.audioContext.sampleRate) const lowFreqRangeEnd = Math.floor((this.midFrequency * this.bufferLength) / this.audioContext.sampleRate) const lowAvg = this.normalizeValue(this.calculateAverage(this.frequencyArray, lowFreqRangeStart, lowFreqRangeEnd)); نفس الطريقة لنطاق الترددات المرتفعة بين MID وHIGH// كشف الإيقاع باستخدام Web Audio Beat Detector حسبنا في الفقرة السابقة قيمة سعة الترددات لكنها لا تكفي لوحدها لربط إيقاعات الموسيقى بالتأثيرات المرئية، سنحتاج لمعرفة قيمة الإيقاع BPM أي عدد النبضات في الدقيقة فهذه القيمة أساسية لجعل العناصر تتفاعل بشكل متزامن مع نبضات الموسيقى. لحسابها سنعتمد على استخدام وحدة جافا سكريبت مفتوحة المصدر باسم web audio beat detector حيث نحتاج لتمرير القيمة audioBuffer لهذه الوحدة وحساب الإيقاع بشكل غير متزامن كما يلي: const { bpm } = await guess(audioBuffer); إرسال الإشارات بعد أن كشفنا قيمة الإيقاع BPM يمكننا إرسال إشارة بوقوع حدث بشكل متكرر عند كل نبضة إيقاع باستخدام الدالة setInterval. this.interval = 60000 / bpm; // Convert BPM to interval this.intervalId = setInterval(() => { this.dispatchEvent({ type: 'beat' }) }, this.interval); إنشاء الأشكال المرئية المتفاعلة ديناميكيًا مع الصوت سننشئ الآن أشكال لجزيئات ديناميكية لتتفاعل مع الصوت. سنبدأ بتعريف دالتين لإنشاء أشكال هندسية بسيطة، سننشئ مكعب Box وأسطوانة Cylinder بأجزاء وخواص عشوائية لتكون الأشكال الأساسية للجزئيات، سينتج عن هذه الطريقة هيكل مميز في كل مرة. ثم سنضيف هاتين الدالتين لكائن Three.Points مع ShaderMaterial بسيط. const geometry = new THREE.BoxGeometry(1, 1, 1, widthSeg, heightSeg, depthSeg) const material = new THREE.ShaderMaterial({ side: THREE.DoubleSide, vertexShader: vertex, fragmentShader: fragment, transparent: true, uniforms: { size: { value: 2 }, }, }) const pointsMesh = new THREE.Points(geometry, material) يمكننا الآن إنشاء تمثيلات الهندسية بسمات عشوائية بفترة زمنية محددة، لاحظ الشكل التالي: توضيح مفهوم Vertex Shader و Fragment Shader سنستخدم في فقراتنا التالية مفهومي مظلل الرؤوس Vertex Shader ومظلل الشرائح Fragment Shader في مكتبة Three.js لتخصيص وتعديل مظهر الكائنات الرسومية ثلاثية الأبعاد. فمظلل الرؤوس Vertex Shader هو جزء من الكود يُنفَّذ لكل رأس Vertex في الكائن ثلاثي الأبعاد ونستخدمه لتعديل موقع الرأس بناءً على المعاملات المرسلة من جافا سكريبت، وتغيير شكل الكائن عن طريق تعديل مواقع الرؤوس كتحريكها أو تدويرها ولكن لا يمكن من خلاله تعديل ألوان البكسلات أو التفاصيل الدقيقة الأخرى للكائن. من ناحية أخرى، يعمل مظلل الشرائح Fragment Shader على تحديد لون كل جزء صغير أو بكسل في الكائن ثلاثي الأبعاد، وهو يحدد اللون النهائي لكل بكسل بناءً على القيم المرسلة من Vertex Shader. ويمكن ضمنه تطبيق مؤثرات لونية على البكسلات كتغيير الألوان أو إضافة تأثيرات مثل الإضاءة أو الظلال. ومن خلال دمج Vertex Shader و Fragment Shader يمكن تحديد مظهر الكائن، حيث يُسمح بتخصيص الشكل والألوان بشكل مرن مما يوفر إمكانيات غير محدودة لإبداع مؤثرات مرئية مميزة. تحريك جزئيات الشكل عشوائيًا سندرج ضوضاء دورانية Curl Noise للمظلل vertex shader لتحريك جزئيات الشكل بحركات طبيعية لها وإضافة حركات دوامية وسلسة لها. حيث سنحرك هذه الجزيئات وننشئ تأثيرات ديناميكية تتحكم بمظهرها وسلوكها. سنبدأ بتحديد موقع هدف نسميه newpos لكل نقطة، ثم نضيف ضوضاء دورانية Cur Noise على طول الشعاع الطبيعي لها ونغير هذه الضوضاء اعتمادا على التردد frequency والسعة amplitude، سيعدل تأثير الضوضاء اللولبية بناءً على المسافة بين النقطة الحالية التي نحركها والهدف d وينتج تحرك سلس يتلاشى تدريجيًا بمعنى أن التغيير يعتمد على المسافة، سيكون التأثير أكبر عندما تكون المسافة صغيرة، ويتناقص تدريجيًا كلما اقتربت النقطة من الهدف بسبب تداخل المسافة. vec3 newpos = position; vec3 target = position + (normal * .1) + curl(newpos.x * frequency, newpos.y * frequency, newpos.z * frequency) * amplitude; float d = length(newpos - target) / maxDistance; newpos = mix(position, target, pow(d, 4.)); سنضيف أيضا حركة متموجة إلى newpos.z لإضافة مزيد من الحيوية للنقاط المتحركة. newpos.z += sin(time) * (.1 * offsetGain); علاوة على ذلك يتغير حجم كل نقطة ديناميكيًا بناءً على مدى قرب النقطة من الهدف وعمقها في المشهد، مما يجعل حركة النقاط تبدو ثلاثية أبعاد أكثر. gl_PointSize = size + (pow(d,3.) * offsetSize) * (1./-mvPosition.z); ستبدو النتيجة النهائية للحركة العشوائية كالتالي: إضافة الألوان سنخفي النقطة في مظلل الشرائح fragment shader باستخدام دالة تنشئ شكل دائري ثم نحدد تدرج لوني بين اللونين startColor و endColor بناءً على المسافة vDistance بين النقطة والهدف والتي حسبناها في المظلل vertex shader. هذا يعني أن الألوان ستتغير تدريجيًا حسب المسافة أيضًا، مما ينتج تأثيرًا مرئيًا سلسًا عند تحرك النقطة. vec3 circ = vec3(circle(uv,1.)); vec3 color = mix(startColor,endColor,vDistance); gl_FragColor=vec4(color,circ.r * vDistance); تظهر النتيجة على النحو التالي: دمج الصوت مع المؤثرات المرئية سنستخدم الآن إبداعنا لتخصيص بيانات الصوت والإيقاع لكل الخصائص التي نريد تغييرها، سواء في مظلل الرؤوس الذي يتحكم في الشكل والموقع أو في مظلل الشرائح الذي يتحكم في الألوان والتأثيرات. يمكننا أيضًا إضافة بعض الرسوم المتحركة العشوائية لتغيير الحجم والموقع position، والدوران باستخدام مكتبة GSAP، وهي مكتبة تساعد في جعل الأشياء تتحرك وتتغير بسلاسة في الوقت الحقيقي. update() { // تحديث السعة ديناميكيًا بناءً على بيانات التردد العالي من مدير الصوت this.material.uniforms.amplitude.value = 0.8 + THREE.MathUtils.mapLinear(App.audioManager.frequencyData.high, 0, 0.6, -0.1, 0.2) // تحديث الإزاحة بناءً على بيانات التردد المنخفض لتغييرات تأثيرات طفيفة this.material.uniforms.offsetGain.value = App.audioManager.frequencyData.mid * 0.6 // تحويل بيانات التردد المنخفض إلى نطاق معين واستخدامه لزيادة قيمة الوقت const t = THREE.MathUtils.mapLinear(App.audioManager.frequencyData.low, 0.6, 1, 0.2, 0.5) this.time += THREE.MathUtils.clamp(t, 0.2, 0.5) // تقييد القيمة للتأكد من بقائها ضمن النطاق المطلوب this.material.uniforms.time.value = this.time } الخلاصة شرحنا في هذا المقال كيفية إنشاء تأثيرات صوتية مزامنة للإيقاع باستخدام مكتبة Three.js. يمكنكم بعد قراءة هذا المقال استخدام Three.js لتحريك الجزيئات بطريقة ديناميكية و مزامنة الصوت وإنشاء مؤثرات مرئية مزامنة للإيقاع والتحكم بها. ترجمة -وبتصرف- لمقال Creating Audio-Reactive Visuals with Dynamic Particles in Three.js لصاحبه Tiago Canzian. اقرأ أيضًا مقدمة إلى WebGL - تعريف - جافاسكربت مقدمة إلى WebGL - التنقل في الفضاء وتشغيل الأضواء سلسلة الوعود Promises chaining في جافاسكربت معالجة المشاكل الشائعة للتوافق مع المتصفحات في شيفرة جافاسكربت
-
تُستخدم واجهات برمجة التطبيقات APIs على نطاق واسع في شتى المجالات، إذ تتيح للبرمجيات إمكانية التواصل والتكامل مع أنظمة وبرمجيات أخرى، سواء كانت داخلية ضمن نفس النظام أو خارجية. وتكمن إحدى أبرز فوائدها في تسهيل إعادة استخدام المكونات البرمجية، وتوفّر معظم الخدمات الإلكترونية عبر الإنترنت واجهات برمجة تطبيقات تمكّن المطورين من دمج ميزات متنوعة، مثل تسجيل الدخول واستخدام حسابات التواصل الاجتماعي، والدفع بواسطة بطاقات الائتمان، وتتبع سلوك المستخدمين بكل سهولة. ويُعد معيار نقل الحالة التمثيلية Representational State Transfer أو REST اختصارًا المعيار الأكثر شيوعًا في تصميم هذه الواجهات. سنشرح في هذا المقال طريقة إنشاء REST API خاصة ببيئة Node.js فلغة جافا سكريبت JavaScript من أكثر لغات البرمجة شيوعًا بين المطورين المحترفين. لذلك سنركز في هذا المقال على إنشاء REST API بسيطة وآمنة، ويمكن بالطبع استخدام أي من المنصات واللغات البرمجية الأخرى لتحقيقها، مثل إطار عمل ASP.NET Core، أو إطار لارافيل Laravel للغة PHP، أو إطار Bottle للغة Python لإنشاء واجهة REST API. متطلبات العمل يحتاج فهم وتطبيق هذا المقال لامتلاك الأمور التالية: خبرة مسبقة في التعامل مع إطار Node.js معرفة بإطار Express.js لأنه معيار أساسي في بناء الواجهة الخلفية للواجهة البرمجية REST API معرفة بمكتبة Mongoose لربط الواجهة الخلفية بقاعدة بيانات MongoDB ملاحظة: لن نتناول في هذا المقال شرح أكواد الواجهة الأمامية، وسنركز على الواجهة الخلفية فقط. ولكن بما أننا سنستخدم لغة JavaScript في تطوير الواجهة الخلفية، فإن هذا يتيح لنا إمكانية مشاركة بعض الأجزاء البرمجية مثل نماذج الكائنات Object Models بين الواجهة الأمامية والخلفية، مما يسهم في توحيد الكود وتقليل التكرار داخل المشروع. هيكلية REST API تستخدم واجهات REST APIs من أجل الوصول للبيانات والتحكم بها، وذلك باستخدام مجموعة من العمليات عديمة الحالة stateless بمعنى أن الخادم ينفذ العملية المطلوبة منه ثم ينسى كل شيء عنها بعد الانتهاء منها. وهذه العمليات جزء لا يتجزأ من بروتوكول HTTP المستخدم في نقل البيانات على الويب، وهي تستخدم في تنفيذ الوظائف الأساسية كإنشاء البيانات وقراءتها وتحديثها وحذفها أو ما يعرف باسم عمليات CRUD، على الرغم من عدم تطابقها بدقة تامة معها. وهذه العمليات هي كالتالي: POST لإنشاء مورد جديد أو توفير البيانات بشكل عام GET للحصول على قائمة موارد أو مورد محدد PUT لإنشاء أو استبدال مورد PATCH لتحديث أو تعديل مورد DELETE لحذف المورد ملاحظة: ما نعنيه بالمورد resource هنا البيانات التي نتعامل معها عبر واجهة برمجة التطبيقات API ويمكننا الوصول إليها عبر عنوان URL لنقطة الوصول endpoint. يتيح لنا بناء واجهة REST API باستخدام Node.js إنشاء نقاط وصول Endpoints لكل عملية من عمليات التحكم بالبيانات، وذلك من خلال استخدام بروتوكول HTTP مع تحديد اسم المورد ضمن عنوان URL. عند اتباع هذا الأسلوب، سنحصل على هيكل واضح ومستقر للواجهة البرمجية يسهل فهمه وتطويره وصيانته بمرونة وسرعة. كما أن هذا النمط يُعد شائعًا بين معظم الخدمات الخارجية التي تعتمد بدورها على REST API، مما يُسهّل عملية التكامل معها لاحقًا، سواء لإضافة ميزات مثل تسجيل الدخول أو الدفع الإلكتروني. والآن، دعونا نبدأ بشرح خطوات بناء REST API آمنة باستخدام Node.js خطوة بخطوة. بناء REST API آمنة لإدارة المستخدمين باستخدام Node.js سنبني في هذا المقال REST API آمنة تتميز بكونها شائعة وعملية لإدارة المستخدمين users. سيملك المورد الذي سننشئه هيكلا بسيطا كالآتي: id: معرف مستخدم مميز UUID منشأ تلقائيًا :firstName: الاسم الأول للمستخدم lastName: الاسم الأخير للمستخدم email: البريد الإلكتروني للمستخدم password: كلمة المرور permissionLevel: أذونات المستخدم كأن يكون مستخدم عادي أو مشرف أو لديه أذونات خاصة ثم سننشئ عمليات لهذا المورد كالآتي: POST عند نقطة الوصول /users لإنشاء مستخدم جديد GET عند نقطة الوصول /users للحصول على قائمة بكافة المستخدمين GET عند نقطة الوصول /users/:userId للحصول على بيانات مستخدم معين PATCH عند نقطة الوصول /users/:userId لتحديث بيانات مستخدم معين DELETE عند نقطة الوصول /users/:userId لحذف مستخدم معين سنستخدم رموز JSON Web Tokens -أو JWTs اختصارًا- كوسلية للتحقق من هوية المستخدمين. عند تسجيل الدخول، سيُطلب من المستخدم إدخال بريده الإلكتروني وكلمة مروره، وسيتم إرسال هذه البيانات إلى مورد خاص بالمصادقة يحمل الاسم auth، بعد التحقق من صحة البيانات، يصدر هذا المورد رمز JWT ويُستخدم لاحقًا للوصول إلى بعض العمليات المحمية داخل الواجهة البرمجية. لمعرفة المزيد عن هذا النوع من الرموز، يمكن الرجوع إلى مقال الاستيثاق عبر مفتاح المستخدم المشفر token authentication في تطبيق Node.js و React إعداد بيئة العمل بداية، علينا التأكد من تثبيت آخر إصدار من Node.js على جهازنا، يستخدم في هذا المقال الإصدار 14.9.0 لكن يمكنك استخدام إصدار أحدث إذا كان متوفرًا. بعد ذلك يجب التأكد من أن MongoDB مثبت أيضَا على جهازنا، لن نخوض في تفاصيل استخدام Mongoose أو هيكلية MongoDB، لكننا سنعتمد على تشغيل الأساسيات فقط. لتبسيط العمل، يمكن تشغيل خادم MongoDB في الوضع التفاعلي، أي باستخدام سطر الأوامر مباشرة بدلًا من تشغيله كخدمة تعمل في الخلفية بكتابة الأمر التالي: mongo سنحتاج لاحقًا في هذا المقال إلى التفاعل المباشر مع MongoDB من خلال هذا الوضع -دون استخدام كود Node.js- لفهم كيفية التعامل مع البيانات. ملاحظة: عند استخدام MongoDB، لا نحتاج لإنشاء قاعدة بيانات محددة كما هو الحال في بعض أنظمة إدارة قواعد البيانات العلاقية RDBMS فسيؤدي أول استدعاء لها من كود Node.js إلى إنشائها تلقائيًا إذا لم تكن موجودة، مما يسهل العمل ويسرع عملية التطوير. لا يحتوي هذا المقال على جميع الأكواد اللازمة لاكتمال عمل المشروع، ويمكنك الحصول على الكود كاملًا من هذا المستودع ومتابعة النقاط المهمة أثناء القراءة، كما يمكن نسخ ملفات ومقاطع محددة من المستودع. بعد ذلك سننتقل إلى ملف rest-api-tutorial/ الناتج في الطرفية، نلاحظ أن المشروع يحتوي على ثلاثة مجلدات لوحدات Modules تنظم مختلف أجزاء التطبيق وهي كالتالي: common لمعالجة جميع الخدمات المشتركة والمعلومات المتبادلة بين المستخدمين users يتضمن كل ما يتعلق بإدارة المستخدمين auth لمعالجة رموز JWT وعمليات تسجيل الدخول بعد ذلك، سنشغل الأمر npm install أو الأمر yarn لتنصيب التبعيات المطلوبة. بمجرد اكتمال عملية التنصيب، سنكون قد أعددنا كل الأمور والتبعيات اللازمة لتشغيل الواجهة الخلفية للواجهة البرمجية REST API باستخدام Node.js. إنشاء وحدة المستخدم User Module سنستخدم مكتبة Mongoose مع MongoDB ، وهي مكتبة تساعدنا في التعامل مع بيانات MongoDB بطريقة مبسطة، وذلك من خلال إنشاء مخطط schema يحدد شكل بيانات المستخدم، ثم استخدامه لبناء نموذج model يسمح لنا بإضافة المستخدمين وتعديلهم وحذفهم من قاعدة البيانات. أولًا، نحتاج لإنشاء مخطط المستخدم User Schema في الملف /users/models/users.model.js كما يلي: const userSchema = new Schema({ firstName: String, lastName: String, email: String, password: String, permissionLevel: Number }); بعد تعريف المخطط يمكننا ربط المخطط بنموذج المستخدم بسهولة كما يلي: const userModel = mongoose.model('Users', userSchema); يمكننا بعدها استخدام هذا النموذج لإضافة كل عمليات CRUD التي نحتاجها في نقاط وصول Express.js الخاصة بنا. لنبدأ بإنشاء مستخدم جديد باستخدام العملية create user عن طريق تعريف وجهة Route في الملف users/routes.config.js: app.post('/users', [ UsersController.insert ]); نقوم بإدراج هذا الجزء من الكود داخل تطبيقنا باستخدام Express.js من خلال الملف الرئيسي index.js، أما كائن UsersController، فنستورده من ملف المتحكم controller المسؤول عن معالجة المستخدمين. وسنشفر كلمة المرور بالشكل المناسب في الملف /users/controllers/user.controller.js لضمان حفظها بأمان في قاعدة البيانات: exports.insert = (req, res) => { let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512',salt) .update(req.body.password) .digest("base64"); req.body.password = salt + "$" + hash; req.body.permissionLevel = 1; UserModel.createUser(req.body) .then((result) => { res.status(201).send({id: result._id}); }); }; عند الوصول لهذه النقطة يمكننا اختبار نموذجنا Mongoose عن طريق تشغيل خادم Node.js API باستخدام الأمر npm start وإرسال طلب POST يحتوي على بعض بيانات JSON إلى نقطة الوصول users/: { "firstName" : "ِِAbd", "lastName" : "Hamza", "email" : "ِِabd.hamza@example.com", "password" : "s3cr3tp4sswo4rd" } هنالك العديد من الأدوات التي يمكننا استخدامها لاختبار واجهتنا البرمجية، سنعتمد على الأداة Insomnia، و يمكن أيضًا استخدام Postman أو أي بدائل مفتوحة المصدر مثل أداة cURL أو Bruno كما يمكننا الاكتفاء بكتابة أكواد جافاسكريبت مباشرة، ككتابة الكود التالي في التبويب Console ضمن أدوات المطور Developer Tools للمتصفح: fetch('http://localhost:3600/users', { method: 'POST', headers: { "Content-type": "application/json" }, body: JSON.stringify({ "firstName": "Abd", "lastName": "Hamza", "email": "abd.hamza@example.com", "password": "s3cr3tp4sswo4rd" }) }) .then(function(response) { return response.json(); }) .then(function(data) { console.log('Request succeeded with JSON response', data); }) .catch(function(error) { console.log('Request failed', error); }); ستحتوي نتيجة طلب POST الناجحة على المعرف id للمستخدم الذي أنشأناه حديثًا: { "id": "5b02c5c84817bf28049e58a3" } سنحتاج أيضا لإضافة التابع createUser إلى النموذج في الملف users/models/users.model.js: exports.createUser = (userData) => { const user = new User(userData); return user.save(); }; الآن علينا التأكد فيما إذا كان المستخدم موجودًا في قاعدة البيانات، لذلك سنضيف ميزة الحصول على اسم المستخدم من خلال المعرّف id إلى نقطة الوصول users/:userId. سننشئ أولًا وجهة Route للتطبيق في الملف /users/routes/config.js: app.get('/users/:userId', [ UsersController.getById ]); ثم سننشئ وحدة تحكم في الملف /users/controllers/users.controller.js: exports.getById = (req, res) => { UserModel.findById(req.params.userId).then((result) => { res.status(200).send(result); }); }; وأخيرا نضيف التابع findById إلى النموذج في الملف /users/models/users.model.js: exports.findById = (id) => { return User.findById(id).then((result) => { result = result.toJSON(); delete result._id; delete result.__v; return result; }); }; يجب أن نحصل على استجابة كما يلي: { "firstName": "Abd", "lastName": "Hamza", "email": "abd.hamza@example.com", "password": "Y+XZEaR7J8xAQCc37nf1rw==$p8b5ykUx6xpC6k8MryDaRmXDxncLumU9mEVabyLdpotO66Qjh0igVOVerdqAh+CUQ4n/E0z48mp8SDTpX2ivuQ==", "permissionLevel": 1, "id": "5b02c5c84817bf28049e58a3" } لاحظ أن بإمكاننا رؤية كلمة المرور المشفرة هنا، ولكن يفضل عدم عرض كلمة المرور مطلقًا حتى ولو شفرناها. كما نلاحظ وجود الحقل permissionLevel وسنستخدمه لاحقًا لتحديد ومعالجة أذونات المستخدم. يمكننا الآن إضافة الوظيفة اللازمة لتحديث بيانات المستخدم مثل الاسم أو البريد الإلكتروني أو كلمة المرور بنفس الطريقة التي استخدمناها سابقًا. سنستخدم هنا عملية PATCH لأنها تسمح لنا بإرسال الحقول المرغوب بتعديلها فقط، ولذلك ستكون ال وجهة Route هو العملية PATCH لنقطة الوصول /users/:userid وسنرسل أي حقول نريد تعديلها في جسم الطلب. سنحتاج أيضًا لإضافة بعض التحققات الإضافية لأن التعديل يجب أن يقتصر على المستخدم المسؤول admin، ولا يجب أن يتمكن أحد غير المسؤول من التعديل على الحقل permisionLevel. ولكننا سنتخطى هذا الأمر حاليًا ونعود له عندما نضيف وحدة المصادقة auth. يجب أن يبدو المتحكم controller الخاص بنا الآن كما يلي: exports.patchById = (req, res) => { if (req.body.password){ let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(req.body.password).digest("base64"); req.body.password = salt + "$" + hash; } UserModel.patchUser(req.params.userId, req.body).then((result) => { res.status(204).send({}); }); }; ترسل هذه الخطوة استجابة برمز الحالة HTTP 204 الذي يشير لنجاح تنفيذ الطلب، ولكن دون إرجاع أي بيانات أو محتوى في جسم الاستجابة. سنحتاج أيضا لإضافة التابع patchUser للنموذج model لتحديث بيانات المستخدم: exports.patchUser = (id, userData) => { return User.findOneAndUpdate({ _id: id }, userData); }; يحقق المتحكم controller التالي عملية GET لعرض قائمة المستخدمين عند نقطة الوصول/users/ مع تطبيق آلية التصفية باستخدام الصفحات والحد الأقصى للعناصر المعروضة: exports.list = (req, res) => { let limit = req.query.limit && req.query.limit <= 100 ? parseInt(req.query.limit) : 10; let page = 0; if (req.query) { if (req.query.page) { req.query.page = parseInt(req.query.page); page = Number.isInteger(req.query.page) ? req.query.page : 0; } } UserModel.list(limit, page).then((result) => { res.status(200).send(result); }) }; سيكون تابع النموذج المقابل كالآتي: exports.list = (perPage, page) => { return new Promise((resolve, reject) => { User.find() .limit(perPage) .skip(perPage * page) .exec(function (err, users) { if (err) { reject(err); } else { resolve(users); } }) }); }; وستكون للاستجابة الناتجة البنية التالية: [ { "firstName": "Maher", "lastName": "Saleh", "email": "maher.saleh@example.com", "password": "z4tS/DtiH+0Gb4J6QN1K3w==$al6sGxKBKqxRQkDmhnhQpEB6+DQgDRH2qr47BZcqLm4/fphZ7+a9U+HhxsNaSnGB2l05Oem/BLIOkbtOuw1tXA==", "permissionLevel": 1, "id": "5b02c5c84817bf28049e58a3" }, { "firstName": "Abd", "lastName": "Hamza", "email": "abd.hamza@example.com", "password": "wTsqO1kHuVisfDIcgl5YmQ==$cw7RntNrNBNw3MO2qLbx959xDvvrDu4xjpYfYgYMxRVDcxUUEgulTlNSBJjiDtJ1C85YimkMlYruU59rx2zbCw==", "permissionLevel": 1, "id": "5b02d038b653603d1ca69729" } ] وأخيرا سنضيف عملية DELETE لحذف المستخدم بناء على المعرف الخاص به عند نقطة الوصول /users/:userId. سننشئ المتحكم controller المسؤول عن الحذف كالآتي: exports.removeById = (req, res) => { UserModel.removeById(req.params.userId) .then((result)=>{ res.status(204).send({}); }); }; كما في السابق، سيرسل المتحكم استجابة بالحالة HTTP 204 الذي يشير لنجاح الطلب. ويكون تابع النموذج model method المقابل كالآتي: exports.removeById = (userId) => { return new Promise((resolve, reject) => { User.deleteMany({_id: userId}, (err) => { if (err) { reject(err); } else { resolve(err); } }); }); }; أصبحنا الآن نملك جميع العمليات اللازمة للتحكم ببيانات المستخدم. إنشاء وحدة المصادقة Auth قبل أن نتمكن من تأمين وحدة المستخدمين users عن طريق إضافة الأذونات والبرامج الوسيطة للتحقق، نحتاج أولًا إلى طريقة تمكننا من توليد رمز مميز JWT صالح للمستخدم الحالي. سيُستخدم هذا الرمز لإثبات هوية المستخدم في الطلبات المستقبلية دون الحاجة لإجراء التحقق في كل مرة. يملك هذا الرمز عادة وقت انتهاء صلاحية ويُنشَأ رمز جديد كل عدة دقائق للحفاظ على تواصل آمن، ولكن في هذا المقال سنتخطى إعادة إنشاء الرمز كل مرة ونكتفي برمز واحد كل تسجيل دخول لتبسيط العمل. سننشئ بداية نقطة وصول تستقبل طلبات POST إلى المورد auth/. يجب أن يحتوي جسم الطلب على البريد الإلكتروني وكلمة المرور الخاصة بالمستخدم على النحو التالي: { "email" : "abd.hamza@example.com", "password" : "s3cr3tp4sswo4rd2" } قبل أن نسمح للطلب بالوصول للمتحكم والتفاعل معه يجب التحقق من أن المستخدم موجود وكلمة مروره صحيحة عن طريق كود يكتب في الملف /authorization/middlewares/verify.user.middleware.js: exports.isPasswordAndUserMatch = (req, res, next) => { UserModel.findByEmail(req.body.email) .then((user)=>{ if(!user[0]){ res.status(404).send({}); }else{ let passwordFields = user[0].password.split('$'); let salt = passwordFields[0]; let hash = crypto.createHmac('sha512', salt) .update(req.body.password) .digest("base64"); if (hash === passwordFields[1]) { req.body = { userId: user[0]._id, email: user[0].email, permissionLevel: user[0].permissionLevel, provider: 'email', name: user[0].firstName + ' ' + user[0].lastName, }; return next(); } else { return res.status(400).send({errors: ['Invalid email or password']}); } } }); }; بعد القيام بذلك، يمكننا الانتقال إلى المتحكم controller وتوليد رمز JWT: exports.login = (req, res) => { try { let refreshId = req.body.userId + jwtSecret; let salt = crypto.randomBytes(16).toString('base64'); let hash = crypto.createHmac('sha512', salt).update(refreshId).digest("base64"); req.body.refreshKey = salt; let token = jwt.sign(req.body, jwtSecret); let b = Buffer.from(hash); let refresh_token = b.toString('base64'); res.status(201).send({accessToken: token, refreshToken: refresh_token}); } catch (err) { res.status(500).send({errors: err}); } }; رغم أننا في هذا المقال لن نعيد إنشاء الرمز المميز JWT بشكل متكرر، إلا أن المتحكم Controller مُصمم ليكون قادرًا على توليد هذه الرموز عند الحاجة. يُعد JWT جزءًا أساسيًا من عملية التحقق من هوية المستخدم، لذلك تم تضمين وظيفة توليد الرمز في المتحكم لتسهيل استخدامها مستقبلًا، سواء في هذا المشروع أو في مشاريع قادمة. كل ما نحتاجه هو إنشاء وجهة واستدعاء البرمجية الوسيطة Middleware المناسبة في الملف /authorization/routes.config.js على النحو التالي: app.post('/auth', [ VerifyUserMiddleware.hasAuthValidFields, VerifyUserMiddleware.isPasswordAndUserMatch, AuthorizationController.login ]); ستحتوي الاستجابة الناتجة على رمز JWT المولد في الحقل accessToken: { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1YjAyYzVjODQ4MTdiZjI4MDQ5ZTU4YTMiLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicGVybWlzc2lvbkxldmVsIjoxLCJwcm92aWRlciI6ImVtYWlsIiwibmFtZSI6Ik1hcmNvIFNpbHZhIiwicmVmcmVzaF9rZXkiOiJiclhZUHFsbUlBcE1PakZIRG1FeENRPT0iLCJpYXQiOjE1MjY5MjMzMDl9.mmNg-i44VQlUEWP3YIAYXVO-74803v1mu-y9QPUQ5VY", "refreshToken": "U3BDQXBWS3kyaHNDaGJNanlJTlFkSXhLMmFHMzA2NzRsUy9Sd2J0YVNDTmUva0pIQ0NwbTJqOU5YZHgxeE12NXVlOUhnMzBWMGNyWmdOTUhSaTdyOGc9PQ==" } بعد إنشاء الرمز، يمكننا استخدامه داخل ترويسة Authorization باستخدام صيغة Bearer ACCESS_TOKEN إنشاء برمجيات وسيطة لعمليات التحقق Validations والأذونات Permissions أول ما يجب علينا فعله هو تحديد من يستطيع استخدام المورد users. وفيما يلي السيناريوهات التي علينا التعامل معها: التسجيل أو إنشاء مستخدم جديد: متاح للجميع، ولا يتطلب استخدام رموز JWT تحديث بيانات مستخدم: مسموح فقط للمستخدم الذي قام بتسجيل الدخول أو للمسؤولين حذف حساب مستخدم: مخصص للمسؤولين فقط بعد تحديد السيناريوهات المحتملة لكيفية تصميم مستويات مختلفة من الوصول بناءً على الأدوار ومتطلبات الأمان في التطبيق، سنحتاج أولًا إلى برمجية وسيطة Middleware تتحقق فيما إذا كان المستخدم يملك رمز JWT صالح. قد تكون عبارة عن برمجية وسيطة بسيطة ضمن الملف /common/middlewares/auth.validation.middleware.js كما يلي: exports.validJWTNeeded = (req, res, next) => { if (req.headers['authorization']) { try { let authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { req.jwt = jwt.verify(authorization[1], secret); return next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } }; سنستخدم رموز الحالة الخاصة ببروتوكول HTTP للتعامل مع الأخطاء التي قد تحدث أثناء معالجة الطلبات، وذلك على النحو التالي: رمز 401 Unauthorized: يُستخدم عندما يكون الطلب غير مصحوب برمز JWT صالح، أو عندما لا يتم إرسال أي رمز على الإطلاق رمز 403 Forbidden: يُستخدم عندما يكون الرمز المرسل صالحًا، لكن المستخدم لا يملك الصلاحيات الكافية لتنفيذ هذا الطلب يمكننا استخدام العملية المنطقية AND أو ما يُعرف بـ قناع البِت Bitmasking، حيث نعتبر كل بت في عدد مكوّن من 32 بت كإذن منفصل. وكل إذن يُمثَّل كقوة للعدد 2. على سبيل المثال، يمكن للمسؤول أن يمتلك جميع الأذونات عن طريق تعيين الرقم 2147483647، مما يعني أن جميع الأذونات مفعّلة. من جهة أخرى، يمكننا تعيين قيمة 7 للأذونات، مما يعني تفعيل الأذونات الموجودة في البتات التي تحمل القيم 1 و 2 و 4، أو ما يعادل القوى 0 و 1 و 2 للعدد 2. وفيما يلي مثال على كيفية استخدام ذلك في برمجية وسيطة Middleware: exports.minimumPermissionLevelRequired = (required_permission_level) => { return (req, res, next) => { let user_permission_level = parseInt(req.jwt.permission_level); let user_id = req.jwt.user_id; if (user_permission_level & required_permission_level) { return next(); } else { return res.status(403).send(); } }; }; تتحقق هذه البرمجية الوسيطة مما إذا كان مستوى أذونات المستخدم يتطابق مع المستوى المطلوب باستخدام العملية AND. إذا كانت النتيجة أكبر من الصفر -أي أن المستخدم يملك الإذن المطلوب- يُسمَح للعملية بالاستمرار عبر الدالة next أما إذا كانت النتيجة صفرًا، فهذا يعني أن المستخدم ليس لديه الأذونات الكافية، وبالتالي ترجع الرمز HTTP 403. الآن، نحتاج إلى إضافة البرمجية الوسيطة الخاصة بالاستيثاق في وجهة وحدة المستخدم الموجودة في الملف /users/routes.config.js، بحيث نضيف التحقق من صلاحيات المستخدم قبل السماح له بالوصول إلى العمليات المختلفة: app.post('/users', [ UsersController.insert ]); app.get('/users', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(PAID), UsersController.list ]); app.get('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.getById ]); app.patch('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(FREE), PermissionMiddleware.onlySameUserOrAdminCanDoThisAction, UsersController.patchById ]); app.delete('/users/:userId', [ ValidationMiddleware.validJWTNeeded, PermissionMiddleware.minimumPermissionLevelRequired(ADMIN), UsersController.removeById ]); هكذا نكون أكملنا التطوير الأساسي لواجهة REST API باستخدام Node.js وكل ما يتبقى هو اختبارها للتأكد من خلوها من الأخطاء. تشغيل واختبار الواجهة البرمجية باستخدام Insomnia سنختبر الواجهة البرمجية باستخدام Insomnia، وهو برنامج عميل REST مناسب يحتوي على نسخة مجانية، ومن الأفضل بالتأكيد تضمين اختبارات الكود وإنشاء تقارير للأخطاء بشكل مناسب داخل المشروع، لكن Insomnia يعد خيارًا جيدًا حاليًا لمجرد اختبار الواجهة البرمجية. لنتمكن من إنشاء مستخدم، نحتاج فقط إلى إرسال طلب POST مع البيانات المطلوبة لنقطة الوصول المناسبة. بعد نجاح الطلب، سنحفظ المعرّف الذي أنشأته لنا الواجهة البرمجية لهذا المستخدم، لاستخدامه لاحقًا في عمليات لاحقة مثل التحديث أو الحذف. يمكننا الآن توليد رمز JWT باستخدام نقطة الوصول /auth/: يجب أن نحصل بنتيجة الطلب على رمز وصول Token كما يلي: الآن، سننسخ هذا الرمز ونضيف قبله كلمة Bearer، مع التأكد من ترك فراغ بين الكلمتين. وسنضيف هذا الرمز لكافة ترويسات الطلب Request Headers ضمن الحقل Authorization. تعرض الصورة التالية استجابة الواجهة البرمجية API عند طلب بيانات مستخدم معين من نقطة الوصول /users/:userId بعد إضافة البرمجية الوسيطة للتحقق من الصلاحيات Permissions Middleware، مع وجود رمز مصادقة صالح Valid Token. كما ذكرنا سابقًا نعرض هنا كلمة المرور لتبسيط الشرح، ولكن لا ننسى أنه لا يجب نهائيًا عرض كلمة المرور في الاستجابات، سواءً كانت مشفرة أم لم تكن. لنحاول الآن الحصول على قائمة بجميع المستخدمين بإرسال الطلب GET على النحو التالي: سنتفاجأ بالحصول على استجابة برمز الخطأ 403: السبب وراء عدم القدرة على الوصول إلى هذه النقطة هو أن مستوى الصلاحيات الحالي للمستخدم هو permissionLevel=1 وهذا لا يمنحه الإذن الكافي للوصول إلى البيانات المطلوبة، لذلك يجب تحديث الصلاحيات permissionLevel لهذا المستخدم من القيمة 1 للقيمة 7، أو ربما للقيمة 5 فهي كافية للوصول إلى نقطة الوصول الحالية. يمكننا القيام بهذا التحديث عبر واجهة MongoDB التفاعلية يدويًا، مع تعديل المعرف ليتناسب مع المستخدم الحالي لدينا كما يلي: db.users.update({"_id" : ObjectId("5b02c5c84817bf28049e58a3")},{$set:{"permissionLevel":5}}) سنحتاج بعد ذلك لتوليد رمز JWT جديد. وبعدها سنحصل على استجابة صحيحة للطلب GET: سنختبر الآن وظيفة تحديث بيانات المستخدم عبر إرسال طلب POST مع بعض الحقول إلى نقطة الوصول /users/:userId: نتوقع الحصول على استجابة بالرمز 204 للتأكد بأن العملية نجحت، ولكن يمكننا التأكد عبر طلب معلومات المستخدم مجددًا. وأخيرا سنحتاج لاختبار حذف المستخدم، للقيام بهذا يجب علينا إنشاء مستخدم جديد، مع عدم نسيان الاحتفاظ برمز المعرف الخاص بالمستخدم والتأكد من امتلاكنا لرمز JWT مناسب لمستخدم مسؤول. يجب علينا تعيين الرقم 2053 إلى userPermission الخاص بالمستخدم الجديد والذي يساوي الرقم 2048، أي ADMIN، بالإضافة إلى الرقم 5 الذي عيناه سابقًا ليتمكن من تنفيذ عملية حذف المستخدم بعد الانتهاء من ذلك وتوليد رمز JWT جديد، يجب علينا تحديث ترويسة طلب Authorization الذي أضفناه سابقًا: بعد إرسال طلب DELETE لنقطة الوصول /users/:userId يفترض أن نحصل على استجابة برمز الحالة 204 لتأكيد نجاح العملية. ويمكننا التأكد مجددا عبر إرسال طلب /users/ لجميع المستخدمين الموجودين من خادم Node API الخاص بنا. الخاتمة تعلمنا في هذا المقال أهم الخطوات اللازمة لإنشاء واجهة برمجة تطبيقات REST API بسيطة وآمنة باستخدام Node.js باستخدام مجموعة من الأدوات والتقنيات، ويجب الانتباه إلى أننا تخطينا هنا اتباع أفضل الممارسات للسهولة ويجب الانتباه للأمور التالية عند تطوير واجهة برمجية فعلية: إضافة تحققات صحيحة وشاملة، كالتأكد من كون البريد الإلكتروني فريد لكل مستخدم إضافة اختبار الوحدة unit testing وتقارير للأخطاء منع المستخدمين من تغيير مستوى الصلاحيات الخاص بهم منع المسؤولين من حذف أنفسهم منع الكشف عن المعلومات الحساسة، ككلمات المرور المشفرة على سبيل المثال نقل رمز JWT من ملف common/config/env.config.js إلى نظام مخصص لتوزيع الرموز السرية خارج الكود البرمجي، بحيث لا يعتمد على البيئة المحلية ترجمة -وبتصرف- لمقال Creating a Secure REST API in Node.js لصاحبه Marcos Henrique da Silva. اقرأ أيضًا اللاتزامن والانتظار async/await في جافاسكربت تعرف على وحدات Node.js الأساسية مدخل إلى الواجهات البرمجية API شرح فلسفة RESTful - تعلم كيف تبني واجهات REST البرمجية مدخل إلى Node.js وExpress
-
Abdulaziz Hamzah بدأ بمتابعة كيف يمكنني كتابة مقالات على المنصة؟
-
لي صديق يكتب وينشر مقالا علد هذه المنصة منصة حسوب، وأرغب ايضا بكتابة مقالاتي الخاصة ونشرها، ولكني لا اجد اي مكان يمكنني اضافة مقال منه، ارجو المساعدة
- 1 جواب
-
- 1
-