fahd-edx-plugin 1.0.0__tar.gz
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.
- fahd_edx_plugin-1.0.0/.gitignore +57 -0
- fahd_edx_plugin-1.0.0/INSTALL.md +402 -0
- fahd_edx_plugin-1.0.0/PKG-INFO +5 -0
- fahd_edx_plugin-1.0.0/fahd_edx/__init__.py +3 -0
- fahd_edx_plugin-1.0.0/fahd_edx/apps.py +53 -0
- fahd_edx_plugin-1.0.0/fahd_edx/middleware.py +165 -0
- fahd_edx_plugin-1.0.0/fahd_edx/settings/__init__.py +1 -0
- fahd_edx_plugin-1.0.0/fahd_edx/settings/common.py +28 -0
- fahd_edx_plugin-1.0.0/fahd_edx/settings/production.py +24 -0
- fahd_edx_plugin-1.0.0/pyproject.toml +35 -0
- fahd_edx_plugin-1.0.0/tutor-fahd/pyproject.toml +31 -0
- fahd_edx_plugin-1.0.0/tutor-fahd/tutorfahd/__init__.py +1 -0
- fahd_edx_plugin-1.0.0/tutor-fahd/tutorfahd/plugin.py +87 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Secrets / env
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
!.env.staging.example
|
|
6
|
+
*.pem
|
|
7
|
+
*.key
|
|
8
|
+
|
|
9
|
+
# Live LMS tenant credentials for the seed script (real values, per-machine /
|
|
10
|
+
# secrets manager). Ignore the dir CONTENTS but keep the tracked .example template
|
|
11
|
+
# (must be `secrets/*` not `secrets/`, or Git can't re-include the example).
|
|
12
|
+
secrets/*
|
|
13
|
+
!secrets/*.example
|
|
14
|
+
|
|
15
|
+
# Python
|
|
16
|
+
__pycache__/
|
|
17
|
+
*.py[cod]
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.coverage
|
|
25
|
+
htmlcov/
|
|
26
|
+
|
|
27
|
+
# uv
|
|
28
|
+
.uv/
|
|
29
|
+
|
|
30
|
+
# Node / Next.js (frontend/)
|
|
31
|
+
node_modules/
|
|
32
|
+
.next/
|
|
33
|
+
out/
|
|
34
|
+
.turbo/
|
|
35
|
+
|
|
36
|
+
# CodeGraph local DB/cache (config is committed; the index is per-machine)
|
|
37
|
+
.codegraph/*.db
|
|
38
|
+
.codegraph/*.db-*
|
|
39
|
+
.codegraph/cache/
|
|
40
|
+
.codegraph/*.log
|
|
41
|
+
.codegraph/.dirty
|
|
42
|
+
|
|
43
|
+
# Eval scorecards are committed in PRs intentionally; ignore only scratch runs
|
|
44
|
+
tests/eval/results/_scratch/
|
|
45
|
+
|
|
46
|
+
# Local dev server logs (backend/frontend run output — per-machine, never committed)
|
|
47
|
+
*.log
|
|
48
|
+
|
|
49
|
+
# OS / editor
|
|
50
|
+
.DS_Store
|
|
51
|
+
Thumbs.db
|
|
52
|
+
*.swp
|
|
53
|
+
|
|
54
|
+
# NOTE: fahd-agent-v1/ is vendored reference code (its nested .git was removed; original history
|
|
55
|
+
# lives at github.com/QusaiiSaleem/eduarabia-agent). It is tracked as plain files in this repo.
|
|
56
|
+
|
|
57
|
+
settings.local.json
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# Fahd for Open edX — Integration Guide
|
|
2
|
+
|
|
3
|
+
> Three methods, from simplest to most integrated. Choose based on your deployment.
|
|
4
|
+
|
|
5
|
+
> **v2 retarget note:** Host URLs below (`https://<your-fahd-v2-host>`) and the Tutor dev
|
|
6
|
+
> default (`http://host.docker.internal:8000`) are **deployment placeholders** — set the real
|
|
7
|
+
> Fahd v2 host per deployment, never hardcode it (Architecture Rule #3). The GitHub remote is
|
|
8
|
+
> `tajahdev/fahd-agent`.
|
|
9
|
+
|
|
10
|
+
| Method | Works On | What You Get | Requires |
|
|
11
|
+
|--------|----------|-------------|----------|
|
|
12
|
+
| **A: LTI 1.3** | Any edX | Fahd in course navigation | Studio UI only |
|
|
13
|
+
| **B: Django Plugin** | Any edX | Floating button on every page | pip install + restart |
|
|
14
|
+
| **C: Tutor Plugin** | Tutor only | Same as B + easy config management | tutor CLI |
|
|
15
|
+
|
|
16
|
+
> **Distribution note:** Packages are currently installed from GitHub. PyPI publishing (`pip install fahd-edx-plugin`) is planned for the stable release.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Emergency Rollback
|
|
21
|
+
|
|
22
|
+
If Fahd breaks your LMS after installation, remove it immediately:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Method B — remove the package and restart:
|
|
26
|
+
pip uninstall fahd-edx-plugin -y
|
|
27
|
+
# Then restart your LMS (tutor local restart lms / systemctl restart edxapp / kubectl rollout restart)
|
|
28
|
+
|
|
29
|
+
# Method C — disable the Tutor plugin:
|
|
30
|
+
tutor plugins disable fahd
|
|
31
|
+
tutor images build openedx
|
|
32
|
+
tutor local launch
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Fahd is a Django middleware — removing it restores your LMS to its original state with zero side effects.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Method A: LTI 1.3 Integration (Simplest — Any Deployment)
|
|
40
|
+
|
|
41
|
+
**No CLI. No downtime. No Docker rebuild.** Done entirely through edX Studio.
|
|
42
|
+
|
|
43
|
+
Fahd appears as a link in course navigation. Students click to open the AI assistant. Authentication is handled via **LTI 1.3** (signed JWT launches, not shared secrets).
|
|
44
|
+
|
|
45
|
+
### Setup
|
|
46
|
+
|
|
47
|
+
1. In **edX Studio** → course → **Settings** → **Advanced Settings**
|
|
48
|
+
2. **LTI Passports:** add `["fahd:YOUR_KEY:YOUR_SECRET"]` _(provided by EduArabia)_
|
|
49
|
+
3. **Advanced Module List:** add `"lti_consumer"` if not already there
|
|
50
|
+
4. Add an **LTI Consumer** component to your course:
|
|
51
|
+
- **LTI ID:** `fahd`
|
|
52
|
+
- **LTI URL:** `https://<your-fahd-v2-host>/lti/launch`
|
|
53
|
+
- **LTI Version:** `LTI 1.3` (Fahd v2 requires real JWKS verification)
|
|
54
|
+
- **Custom Parameters:** `tenant=YOUR_TENANT_ID`
|
|
55
|
+
|
|
56
|
+
> **LTI 1.3:** Fahd v2 uses signed JWTs with full JWKS verification + nonce/replay defense. Contact EduArabia for the OIDC login URL and JWKS endpoint.
|
|
57
|
+
|
|
58
|
+
### Limitations
|
|
59
|
+
|
|
60
|
+
- Only inside courses (not on dashboard, profile, or grades pages)
|
|
61
|
+
- Must be configured per course
|
|
62
|
+
- Student clicks a link (no floating button)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Method B: Django App Plugin (Recommended — Any Deployment)
|
|
67
|
+
|
|
68
|
+
**Works on Tutor, Kubernetes, native installs, managed hosting.** Just pip install and restart.
|
|
69
|
+
|
|
70
|
+
Fahd's floating button appears on **every legacy (server-rendered) HTML page** — dashboard, profile, grades, course about. (MFE/React pages are out of scope — see Known Limitations.)
|
|
71
|
+
|
|
72
|
+
### Step 1: Install inside your edX environment
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Pin to a specific release (recommended):
|
|
76
|
+
pip install "fahd-edx-plugin @ git+https://github.com/tajahdev/fahd-agent.git@v2.0.0#subdirectory=plugins/edx"
|
|
77
|
+
|
|
78
|
+
# Or latest (development only):
|
|
79
|
+
pip install "git+https://github.com/tajahdev/fahd-agent.git#subdirectory=plugins/edx"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Step 2: Verify installation before restarting
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Check package is installed:
|
|
86
|
+
pip show fahd-edx-plugin
|
|
87
|
+
|
|
88
|
+
# Check middleware is importable:
|
|
89
|
+
python -c "from fahd_edx.middleware import FahdMiddleware; print('OK')"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If both commands succeed, the plugin is ready.
|
|
93
|
+
|
|
94
|
+
### Step 3: Register your institution with Fahd
|
|
95
|
+
|
|
96
|
+
Before configuring the plugin, you need to register your edX instance with the Fahd v2 server. This gives you back the `FAHD_AUTH_SECRET` and `FAHD_TENANT_ID` that the plugin needs.
|
|
97
|
+
|
|
98
|
+
**3a. Create OAuth2 credentials in your edX admin:**
|
|
99
|
+
|
|
100
|
+
1. Go to `https://YOUR_EDX_DOMAIN/admin/oauth2_provider/application/add/`
|
|
101
|
+
2. Fill in:
|
|
102
|
+
- **Name:** `Fahd AI Assistant`
|
|
103
|
+
- **Client type:** `Confidential`
|
|
104
|
+
- **Authorization grant type:** `Client credentials`
|
|
105
|
+
- **Redirect URIs:** _(leave blank)_
|
|
106
|
+
3. Save and copy the **Client ID** and **Client Secret**
|
|
107
|
+
|
|
108
|
+
**3b. Register with Fahd (one-time, takes 10 seconds):**
|
|
109
|
+
|
|
110
|
+
EduArabia provides the registration key. Run this command (replace the placeholders):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
curl -X POST https://<your-fahd-v2-host>/api/register \
|
|
114
|
+
-H "X-Registration-Key: <provided by EduArabia>" \
|
|
115
|
+
-H "Content-Type: application/json" \
|
|
116
|
+
-d '{
|
|
117
|
+
"name": "Your University Name",
|
|
118
|
+
"platform": "edx",
|
|
119
|
+
"lms_url": "https://YOUR_EDX_DOMAIN",
|
|
120
|
+
"credentials": {
|
|
121
|
+
"client_id": "<Client ID from step 3a>",
|
|
122
|
+
"client_secret": "<Client Secret from step 3a>"
|
|
123
|
+
}
|
|
124
|
+
}'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**You'll get back:**
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"tenant_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
|
|
132
|
+
"auth_secret": "kT9x2mP...",
|
|
133
|
+
"fahd_url": "https://<your-fahd-v2-host>",
|
|
134
|
+
"status": "syncing"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Save `tenant_id` and `auth_secret` — you need them in the next step.
|
|
139
|
+
|
|
140
|
+
> **Note:** In Fahd v2 the `tenant_id` is a **bare UUID** (e.g.,
|
|
141
|
+
> `3f2504e0-4f89-41d3-9a0c-0305e82c3301`). Use it exactly as returned. Do NOT add a
|
|
142
|
+
> `tenant:` prefix — v2 stores tenant ids as Postgres UUIDs and a prefixed value
|
|
143
|
+
> fails token verification.
|
|
144
|
+
|
|
145
|
+
### Step 4: Set environment variables
|
|
146
|
+
|
|
147
|
+
Use the values from Step 3b:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
export FAHD_URL=https://<your-fahd-v2-host>
|
|
151
|
+
export FAHD_TENANT_ID=<tenant_id from Step 3b>
|
|
152
|
+
export FAHD_AUTH_SECRET=<auth_secret from Step 3b>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Where to set these depends on your deployment:**
|
|
156
|
+
|
|
157
|
+
| Deployment | Where to set |
|
|
158
|
+
|-----------|-------------|
|
|
159
|
+
| **Tutor** | Use Method C instead (easier) |
|
|
160
|
+
| **Kubernetes** | Helm values or ConfigMap |
|
|
161
|
+
| **Native** | `/edx/app/edxapp/edx-platform/lms/envs/private.py` |
|
|
162
|
+
| **Managed hosting** | Ask your provider to set them |
|
|
163
|
+
|
|
164
|
+
### Step 5: Restart LMS
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Tutor:
|
|
168
|
+
tutor local restart lms
|
|
169
|
+
|
|
170
|
+
# Kubernetes:
|
|
171
|
+
kubectl rollout restart deployment/lms
|
|
172
|
+
|
|
173
|
+
# Native:
|
|
174
|
+
sudo systemctl restart edxapp
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Step 6: Verify
|
|
178
|
+
|
|
179
|
+
Open any edX page. The floating blue button should appear in the bottom-right corner.
|
|
180
|
+
|
|
181
|
+
If not, view page source (`Ctrl+U`) and search for `embed.js`.
|
|
182
|
+
|
|
183
|
+
### How auto-discovery works
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
pip install fahd-edx-plugin
|
|
187
|
+
→ Python registers lms.djangoapp entry point
|
|
188
|
+
→ Open edX loads FahdPluginConfig (apps.py)
|
|
189
|
+
→ apps.py declares plugin_app settings
|
|
190
|
+
→ settings/common.py runs → appends FahdMiddleware to MIDDLEWARE
|
|
191
|
+
→ settings/production.py runs → reads FAHD_* from env vars
|
|
192
|
+
→ Middleware injects embed.js into every legacy HTML response
|
|
193
|
+
→ No patches. No Tutor. No Docker rebuild.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Method C: Tutor Plugin (Easiest for Tutor Users)
|
|
199
|
+
|
|
200
|
+
**Wraps Method B** with Tutor config management. Auto-generates the auth secret.
|
|
201
|
+
|
|
202
|
+
### Step 1: Install the Tutor plugin
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Pin to a release (recommended):
|
|
206
|
+
pip install "tutor-fahd @ git+https://github.com/tajahdev/fahd-agent.git@v2.0.0#subdirectory=plugins/edx/tutor-fahd"
|
|
207
|
+
|
|
208
|
+
# Or latest (development only):
|
|
209
|
+
pip install "git+https://github.com/tajahdev/fahd-agent.git#subdirectory=plugins/edx/tutor-fahd"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
> You only install `tutor-fahd` here. It automatically installs `fahd-edx-plugin` inside the Docker container during the next build (Step 3).
|
|
213
|
+
|
|
214
|
+
### Step 2: Register your institution
|
|
215
|
+
|
|
216
|
+
Follow **Method B, Step 3** (3a + 3b) to create OAuth2 credentials and register with Fahd. You'll get back a `tenant_id` and `auth_secret`.
|
|
217
|
+
|
|
218
|
+
### Step 3: Enable and configure
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
tutor plugins enable fahd
|
|
222
|
+
tutor config save --set FAHD_URL=https://<your-fahd-v2-host>
|
|
223
|
+
tutor config save --set FAHD_TENANT_ID=<tenant_id from registration>
|
|
224
|
+
tutor config save --set FAHD_AUTH_SECRET=<auth_secret from registration>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
> The Tutor wrapper's default `FAHD_URL` is `http://host.docker.internal:8000` — a
|
|
228
|
+
> dev convenience that reaches a Fahd v2 server running on your host machine from
|
|
229
|
+
> inside the edX container. Always override it with your real host in production.
|
|
230
|
+
|
|
231
|
+
### Step 4: Build images and launch
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# Build the Docker image first (installs fahd-edx-plugin inside container):
|
|
235
|
+
tutor images build openedx
|
|
236
|
+
|
|
237
|
+
# Then launch (restarts services with new image):
|
|
238
|
+
tutor local launch
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
> **Downtime warning:** `tutor local launch` restarts all services. Expect 5-10 minutes of downtime. Schedule during a maintenance window.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Updating
|
|
246
|
+
|
|
247
|
+
### Method B (Django Plugin)
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
pip install --upgrade "fahd-edx-plugin @ git+https://github.com/tajahdev/fahd-agent.git@v2.1.0#subdirectory=plugins/edx"
|
|
251
|
+
# Restart LMS (no Docker rebuild needed)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Method C (Tutor Plugin)
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
pip install --upgrade "tutor-fahd @ git+https://github.com/tajahdev/fahd-agent.git@v2.1.0#subdirectory=plugins/edx/tutor-fahd"
|
|
258
|
+
tutor images build openedx # Rebuild image with updated fahd-edx-plugin
|
|
259
|
+
tutor local launch # Restart with new image
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Method A (LTI)
|
|
263
|
+
|
|
264
|
+
No updates needed on your side. The Fahd server updates independently.
|
|
265
|
+
|
|
266
|
+
> **Always pin to a version tag** (`@v2.0.0`) in production. Never install from `main` branch on a live LMS.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Post-Install Testing Checklist
|
|
271
|
+
|
|
272
|
+
Run these tests after installation to confirm everything works. Takes ~5 minutes.
|
|
273
|
+
|
|
274
|
+
### Test 1: Plugin is installed
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
pip show fahd-edx-plugin
|
|
278
|
+
# Expected: Name: fahd-edx-plugin
|
|
279
|
+
|
|
280
|
+
python -c "from fahd_edx.middleware import FahdMiddleware; print('OK')"
|
|
281
|
+
# Expected: OK
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Test 2: Floating button appears (admin account)
|
|
285
|
+
|
|
286
|
+
1. Log in as **admin** to your edX LMS
|
|
287
|
+
2. Go to the **Dashboard** page
|
|
288
|
+
3. Look for a floating blue circle button in the bottom-right corner
|
|
289
|
+
|
|
290
|
+
**If no button:** View page source (`Ctrl+U`), search for `embed.js`.
|
|
291
|
+
|
|
292
|
+
### Test 3: Token has correct fields
|
|
293
|
+
|
|
294
|
+
1. View page source on any logged-in page
|
|
295
|
+
2. Find the `<script src="...embed.js" data-token="...">` tag
|
|
296
|
+
3. Copy the `data-token` value (everything before the dot)
|
|
297
|
+
4. Decode it:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
echo "<paste the part BEFORE the dot>" | python3 -c "
|
|
301
|
+
import base64, json, sys
|
|
302
|
+
token_part = sys.stdin.read().strip()
|
|
303
|
+
payload = json.loads(base64.urlsafe_b64decode(token_part + '=='))
|
|
304
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
305
|
+
"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Expected fields:**
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"cid": "", ← empty on dashboard, course ID on course pages
|
|
312
|
+
"cname": "",
|
|
313
|
+
"exp": 1710600000, ← ~1 hour in the future
|
|
314
|
+
"iat": 1710596400, ← current timestamp
|
|
315
|
+
"lang": "ar",
|
|
316
|
+
"platform": "edx",
|
|
317
|
+
"roles": ["admin"], ← matches your role
|
|
318
|
+
"tid": "3f2504e0-4f89-41d3-9a0c-0305e82c3301", ← your tenant UUID (BARE — no "tenant:" prefix)
|
|
319
|
+
"uid": "2" ← your edX user ID
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Test 4: Student sees their own context
|
|
324
|
+
|
|
325
|
+
Log in as a **student**, decode the token, and confirm `roles` shows `["student"]`.
|
|
326
|
+
|
|
327
|
+
### Test 5: Anonymous users don't see Fahd
|
|
328
|
+
|
|
329
|
+
Open an incognito window on the login page — there should be **no** `embed.js` script tag.
|
|
330
|
+
|
|
331
|
+
### Test 6: Chat works
|
|
332
|
+
|
|
333
|
+
Log in, click the floating button, send a message. Fahd should respond.
|
|
334
|
+
|
|
335
|
+
**If chat shows "401" or "unauthorized":** The `FAHD_AUTH_SECRET` doesn't match what registration returned.
|
|
336
|
+
|
|
337
|
+
### Test 7: Course context detected
|
|
338
|
+
|
|
339
|
+
Navigate to a course page and confirm `cid` now contains the course ID (e.g., `course-v1:Org+Course+Run`).
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Troubleshooting
|
|
344
|
+
|
|
345
|
+
### Chat shows 401 error
|
|
346
|
+
|
|
347
|
+
`FAHD_AUTH_SECRET` mismatch. The value in your edX plugin must exactly match what the registration API returned.
|
|
348
|
+
|
|
349
|
+
- **Method B:** Check: `echo $FAHD_AUTH_SECRET`
|
|
350
|
+
- **Method C:** Check: `tutor config printvalue FAHD_AUTH_SECRET`
|
|
351
|
+
|
|
352
|
+
### Fahd server is down — does it affect edX?
|
|
353
|
+
|
|
354
|
+
**No.** The script loads with `defer` (non-blocking). If the Fahd server is unreachable, the button appears grayed out. The middleware adds ~1ms overhead (string replacement only, zero network calls).
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Known Limitations
|
|
359
|
+
|
|
360
|
+
### MFE (React) pages not covered — Phase 2 / out of scope
|
|
361
|
+
|
|
362
|
+
The middleware only injects `embed.js` into **server-rendered Django (legacy) pages**
|
|
363
|
+
(dashboard, profile, grades, course about, admin). It does **NOT** appear on **MFE
|
|
364
|
+
React pages** (Learning MFE, Studio MFE, Discussions MFE, etc.) — those are separate
|
|
365
|
+
React apps served as static files that Django middleware cannot reach.
|
|
366
|
+
|
|
367
|
+
MFE coverage via Frontend Plugin Slots is **phase-2 / out of scope** for this port.
|
|
368
|
+
The middleware deliberately only touches `text/html` responses; JSON / MFE responses
|
|
369
|
+
pass through untouched (there is a test for this:
|
|
370
|
+
`tests/integration/test_edx_token_verifies_on_v2.py::test_json_response_not_modified`).
|
|
371
|
+
|
|
372
|
+
**What's covered now:** Dashboard, Profile, Grades, Account Settings, Course About,
|
|
373
|
+
Admin pages, any server-rendered page.
|
|
374
|
+
|
|
375
|
+
**What's NOT covered:** Learning MFE (courseware), Studio MFE, Discussions MFE.
|
|
376
|
+
|
|
377
|
+
> For in-course access, use **Method A (LTI)** alongside Method B.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Privacy & Data
|
|
382
|
+
|
|
383
|
+
The middleware generates a signed token containing only:
|
|
384
|
+
|
|
385
|
+
| Field | Value | Example |
|
|
386
|
+
|-------|-------|---------|
|
|
387
|
+
| `uid` | edX user ID | `42` |
|
|
388
|
+
| `roles` | Role in current context | `["student"]` |
|
|
389
|
+
| `tid` | Tenant UUID (bare) | `3f2504e0-4f89-41d3-9a0c-0305e82c3301` |
|
|
390
|
+
| `cid` | Course ID (if in a course) | `course-v1:KSU+CS101+2026` |
|
|
391
|
+
| `lang` | Interface language | `ar` |
|
|
392
|
+
| `platform` | Always "edx" | `edx` |
|
|
393
|
+
| `exp` | Token expiry (1 hour) | `1710600000` |
|
|
394
|
+
|
|
395
|
+
**No passwords, emails, or personal information** are included in the token.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Support
|
|
400
|
+
|
|
401
|
+
- **Email:** support@eduarabia.com
|
|
402
|
+
- **Documentation:** https://docs.eduarabia.com/fahd
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# FILE: fahd_edx/apps.py
|
|
3
|
+
# PURPOSE: Django App Plugin registration for Open edX
|
|
4
|
+
#
|
|
5
|
+
# HOW THIS WORKS:
|
|
6
|
+
# Open edX discovers this via the lms.djangoapp / cms.djangoapp
|
|
7
|
+
# entry points in pyproject.toml. At startup, it reads plugin_app
|
|
8
|
+
# and auto-loads our settings (which register the middleware).
|
|
9
|
+
#
|
|
10
|
+
# No Tutor patches needed. No manual MIDDLEWARE.append().
|
|
11
|
+
# Just: pip install fahd-edx-plugin → restart → Fahd appears.
|
|
12
|
+
#
|
|
13
|
+
# PATTERN FROM:
|
|
14
|
+
# - github.com/openedx/sample-plugin (official reference)
|
|
15
|
+
# - github.com/mitodl/open-edx-plugins (MIT production)
|
|
16
|
+
# ============================================================
|
|
17
|
+
|
|
18
|
+
from django.apps import AppConfig
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from edx_django_utils.plugins.constants import PluginSettings
|
|
22
|
+
except ImportError:
|
|
23
|
+
# Fallback if edx-django-utils is not installed (e.g., local testing).
|
|
24
|
+
# These are just string constants — the actual values don't matter
|
|
25
|
+
# as long as they match what edx-django-utils expects.
|
|
26
|
+
class PluginSettings:
|
|
27
|
+
CONFIG = "settings_config"
|
|
28
|
+
RELATIVE_PATH = "relative_path"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FahdPluginConfig(AppConfig):
|
|
32
|
+
"""
|
|
33
|
+
Django App Plugin: Fahd AI assistant for Open edX.
|
|
34
|
+
|
|
35
|
+
Auto-discovered via lms.djangoapp / cms.djangoapp entry points.
|
|
36
|
+
Just pip install and restart — no Tutor patches needed.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name = "fahd_edx"
|
|
40
|
+
verbose_name = "Fahd AI Assistant"
|
|
41
|
+
|
|
42
|
+
plugin_app = {
|
|
43
|
+
PluginSettings.CONFIG: {
|
|
44
|
+
"lms.djangoapp": {
|
|
45
|
+
"common": {PluginSettings.RELATIVE_PATH: "settings.common"},
|
|
46
|
+
"production": {PluginSettings.RELATIVE_PATH: "settings.production"},
|
|
47
|
+
},
|
|
48
|
+
"cms.djangoapp": {
|
|
49
|
+
"common": {PluginSettings.RELATIVE_PATH: "settings.common"},
|
|
50
|
+
"production": {PluginSettings.RELATIVE_PATH: "settings.production"},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# FILE: fahd_edx/middleware.py
|
|
3
|
+
# PURPOSE: Inject Fahd embed.js into every legacy HTML page
|
|
4
|
+
#
|
|
5
|
+
# COVERS: Dashboard, Grades, Course About, Profile, Admin, etc.
|
|
6
|
+
# DOES NOT COVER: MFE React pages (Phase 2 — footer slot)
|
|
7
|
+
#
|
|
8
|
+
# HOW IT WORKS:
|
|
9
|
+
# 1. Django renders an HTML response
|
|
10
|
+
# 2. This middleware intercepts it
|
|
11
|
+
# 3. If user is authenticated and Fahd is configured:
|
|
12
|
+
# - Extract user identity (uid, role, course)
|
|
13
|
+
# - Generate HMAC-SHA256 token (same format as Moodle/BB plugins)
|
|
14
|
+
# - Inject <script src="embed.js" data-token="..."> before </head>
|
|
15
|
+
# 4. embed.js creates the floating Fahd button
|
|
16
|
+
#
|
|
17
|
+
# PERFORMANCE:
|
|
18
|
+
# - ~1ms overhead (string replacement, no network calls)
|
|
19
|
+
# - Script loads with `defer` (non-blocking)
|
|
20
|
+
# - If Fahd server is down, button shows grayed out (no page impact)
|
|
21
|
+
#
|
|
22
|
+
# TOKEN FORMAT (must match v2 core/auth.py and PHP token_generator.php):
|
|
23
|
+
# - json.dumps with ensure_ascii=False + sort_keys=True + COMPACT separators
|
|
24
|
+
# - PHP equivalent: ksort() + JSON_UNESCAPED_UNICODE (compact, no spaces)
|
|
25
|
+
#
|
|
26
|
+
# PORTED FROM fahd-agent-v1/plugins/edx/fahd_edx/middleware.py. TWO semantic
|
|
27
|
+
# changes only (see _create_token): (a) `tid` is the BARE tenant id (v1's
|
|
28
|
+
# "tenant:" prefix breaks v2 auth's CAST(:tid AS uuid)); (b) compact JSON
|
|
29
|
+
# separators (v1 used the default spaced separators, which were NOT byte-equal
|
|
30
|
+
# to v2 core/auth.py — the latent v1 byte-equivalence bug that v2 fixed).
|
|
31
|
+
# ============================================================
|
|
32
|
+
|
|
33
|
+
import base64
|
|
34
|
+
import hashlib
|
|
35
|
+
import hmac
|
|
36
|
+
import html
|
|
37
|
+
import json
|
|
38
|
+
import re
|
|
39
|
+
import time
|
|
40
|
+
|
|
41
|
+
from django.conf import settings as django_settings
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FahdMiddleware:
|
|
45
|
+
"""Inject Fahd embed.js into every HTML response."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, get_response):
|
|
48
|
+
self.get_response = get_response
|
|
49
|
+
self.fahd_url = getattr(django_settings, "FAHD_URL", "")
|
|
50
|
+
self.tenant_id = getattr(django_settings, "FAHD_TENANT_ID", "")
|
|
51
|
+
self.auth_secret = getattr(django_settings, "FAHD_AUTH_SECRET", "")
|
|
52
|
+
|
|
53
|
+
def __call__(self, request):
|
|
54
|
+
response = self.get_response(request)
|
|
55
|
+
|
|
56
|
+
if "text/html" not in response.get("Content-Type", ""):
|
|
57
|
+
return response
|
|
58
|
+
if not hasattr(request, "user") or not request.user.is_authenticated:
|
|
59
|
+
return response
|
|
60
|
+
# Fail-closed: a misconfigured install missing the URL, signing secret, OR
|
|
61
|
+
# tenant id injects NOTHING rather than minting permanently-invalid tokens
|
|
62
|
+
# (e.g. a "default" tid that no tenant row will ever match).
|
|
63
|
+
if not self.fahd_url or not self.auth_secret or not self.tenant_id:
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
user = request.user
|
|
67
|
+
course_id = self._extract_course_id(request.path)
|
|
68
|
+
role = self._detect_role(user, course_id)
|
|
69
|
+
token = self._create_token(user, role, course_id)
|
|
70
|
+
|
|
71
|
+
# XSS defense-in-depth: escape settings-sourced values placed into HTML
|
|
72
|
+
# attributes (mirrors the M5-T01 view.php s() fix). `token` is base64url and
|
|
73
|
+
# therefore already attribute-safe (its alphabet excludes <>&"'), so it is
|
|
74
|
+
# left as-is.
|
|
75
|
+
safe_url = html.escape(self.fahd_url, quote=True)
|
|
76
|
+
safe_tenant = html.escape(self.tenant_id, quote=True)
|
|
77
|
+
script = (
|
|
78
|
+
f'<script src="{safe_url}/embed.js" '
|
|
79
|
+
f'data-tenant="{safe_tenant}" '
|
|
80
|
+
f'data-token="{token}" data-lang="ar" defer></script>'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Guard non-UTF-8 legacy pages: if the body is not valid UTF-8, pass it
|
|
84
|
+
# through unchanged (no injection) rather than 500-ing on every such page.
|
|
85
|
+
try:
|
|
86
|
+
content = response.content.decode("utf-8")
|
|
87
|
+
except UnicodeDecodeError:
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
# Inject ONCE, before the FIRST `</head>` only. replace-all would also
|
|
91
|
+
# rewrite stray `</head>` occurrences inside JS strings or <pre> blocks.
|
|
92
|
+
content = content.replace("</head>", f"{script}\n</head>", 1)
|
|
93
|
+
response.content = content.encode("utf-8")
|
|
94
|
+
response["Content-Length"] = len(response.content)
|
|
95
|
+
return response
|
|
96
|
+
|
|
97
|
+
def _extract_course_id(self, path):
|
|
98
|
+
"""Extract course ID from URL path if inside a course."""
|
|
99
|
+
match = re.search(r"/courses/(course-v1:[^/]+)", path)
|
|
100
|
+
return match.group(1) if match else ""
|
|
101
|
+
|
|
102
|
+
def _detect_role(self, user, course_id):
|
|
103
|
+
"""Detect user's role (student/teacher/admin) in the course."""
|
|
104
|
+
if user.is_staff or user.is_superuser:
|
|
105
|
+
return "admin"
|
|
106
|
+
if course_id:
|
|
107
|
+
try:
|
|
108
|
+
from opaque_keys.edx.keys import CourseKey
|
|
109
|
+
from student.roles import CourseInstructorRole, CourseStaffRole
|
|
110
|
+
|
|
111
|
+
key = CourseKey.from_string(course_id)
|
|
112
|
+
if CourseInstructorRole(key).has_user(user) or CourseStaffRole(key).has_user(user):
|
|
113
|
+
return "teacher"
|
|
114
|
+
except Exception:
|
|
115
|
+
# Graceful degradation (Architecture Rule #15): ANY failure here —
|
|
116
|
+
# a malformed course_id, the edX role imports being absent (e.g. when
|
|
117
|
+
# this middleware runs outside a full edX install), or a role-lookup
|
|
118
|
+
# DB error — degrades to "student". This token only carries a UI hint;
|
|
119
|
+
# the adapter's get_user_role stays the source of truth (Rule #9), so
|
|
120
|
+
# under-claiming the role here cannot grant unauthorized access.
|
|
121
|
+
pass
|
|
122
|
+
return "student"
|
|
123
|
+
|
|
124
|
+
def _create_token(self, user, role, course_id):
|
|
125
|
+
"""Generate HMAC-SHA256 signed token for Fahd.
|
|
126
|
+
|
|
127
|
+
TWO v1->v2 retarget changes (the rest is verbatim from v1):
|
|
128
|
+
|
|
129
|
+
1. BARE `tid`. v1 set `tid = f"tenant:{self.tenant_id}"` (a SurrealDB
|
|
130
|
+
record-id convention). v2 auth does `CAST(:tid AS uuid)`, which raises
|
|
131
|
+
on `"tenant:<uuid>"`, so a prefixed token would fail
|
|
132
|
+
verify_token_for_tenant. We emit the bare tenant id, matching v2
|
|
133
|
+
core/auth.py and core/moodle_bridge.build_fahd_payload.
|
|
134
|
+
|
|
135
|
+
2. COMPACT separators=(",", ":"). v1 signed
|
|
136
|
+
`json.dumps(payload, ensure_ascii=False, sort_keys=True)` with the
|
|
137
|
+
DEFAULT separators (a space after every ":" and ","). PHP's
|
|
138
|
+
json_encode and v2 core/auth.py both emit COMPACT JSON (no spaces),
|
|
139
|
+
so v1's bytes — and therefore its HMAC — did NOT match. This is the
|
|
140
|
+
latent v1 byte-equivalence bug that v2 fixed; the compact separators
|
|
141
|
+
here make the inline token byte-identical to create_fahd_token.
|
|
142
|
+
"""
|
|
143
|
+
now = int(time.time())
|
|
144
|
+
payload = {
|
|
145
|
+
"uid": str(user.id),
|
|
146
|
+
"roles": [role],
|
|
147
|
+
"tid": self.tenant_id,
|
|
148
|
+
"cid": course_id,
|
|
149
|
+
"cname": "",
|
|
150
|
+
"lang": "ar",
|
|
151
|
+
"platform": "edx",
|
|
152
|
+
"iat": now,
|
|
153
|
+
"exp": now + 3600,
|
|
154
|
+
}
|
|
155
|
+
# CRITICAL: must match v2 core/auth.py — ensure_ascii=False + sort_keys=True
|
|
156
|
+
# + COMPACT separators (no spaces). The separators are the load-bearing fix.
|
|
157
|
+
payload_bytes = json.dumps(
|
|
158
|
+
payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
159
|
+
).encode("utf-8")
|
|
160
|
+
sig = hmac.new(self.auth_secret.encode(), payload_bytes, hashlib.sha256).digest()
|
|
161
|
+
return (
|
|
162
|
+
base64.urlsafe_b64encode(payload_bytes).rstrip(b"=").decode()
|
|
163
|
+
+ "."
|
|
164
|
+
+ base64.urlsafe_b64encode(sig).rstrip(b"=").decode()
|
|
165
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Fahd edX plugin settings package (loaded by Open edX via apps.py plugin_app)."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# FILE: fahd_edx/settings/common.py
|
|
3
|
+
# PURPOSE: Register Fahd middleware and set config defaults
|
|
4
|
+
#
|
|
5
|
+
# HOW THIS WORKS:
|
|
6
|
+
# Open edX calls plugin_settings(settings) during startup.
|
|
7
|
+
# We append our middleware to the MIDDLEWARE list and set
|
|
8
|
+
# default config values. No Tutor patches needed.
|
|
9
|
+
#
|
|
10
|
+
# This runs for BOTH LMS and CMS (Studio).
|
|
11
|
+
# ============================================================
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plugin_settings(settings):
|
|
15
|
+
"""
|
|
16
|
+
Called by Open edX during startup.
|
|
17
|
+
Appends Fahd middleware and sets config defaults.
|
|
18
|
+
"""
|
|
19
|
+
# Register middleware — injects embed.js into every HTML page
|
|
20
|
+
settings.MIDDLEWARE.append("fahd_edx.middleware.FahdMiddleware")
|
|
21
|
+
|
|
22
|
+
# Config defaults (overridden by production.py or env vars)
|
|
23
|
+
settings.FAHD_URL = getattr(settings, "FAHD_URL", "http://localhost:8000")
|
|
24
|
+
# Fail-closed default: empty (not the non-UUID sentinel "default") so the
|
|
25
|
+
# middleware guard (`not self.tenant_id`) skips injection on a misconfigured
|
|
26
|
+
# install instead of minting tokens that fail server-side CAST(:tid AS uuid).
|
|
27
|
+
settings.FAHD_TENANT_ID = getattr(settings, "FAHD_TENANT_ID", "")
|
|
28
|
+
settings.FAHD_AUTH_SECRET = getattr(settings, "FAHD_AUTH_SECRET", "")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# FILE: fahd_edx/settings/production.py
|
|
3
|
+
# PURPOSE: Production overrides — reads config from environment
|
|
4
|
+
#
|
|
5
|
+
# HOW THIS WORKS:
|
|
6
|
+
# Open edX calls plugin_settings(settings) AFTER common.py.
|
|
7
|
+
# We override defaults with environment variables if set.
|
|
8
|
+
#
|
|
9
|
+
# ENVIRONMENT VARIABLES:
|
|
10
|
+
# FAHD_URL — Fahd server URL (e.g., https://fahd.eduarabia.com)
|
|
11
|
+
# FAHD_TENANT_ID — Institution tenant ID
|
|
12
|
+
# FAHD_AUTH_SECRET — HMAC signing secret (must match Fahd server)
|
|
13
|
+
# ============================================================
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def plugin_settings(settings):
|
|
19
|
+
"""
|
|
20
|
+
Production overrides — reads from environment variables.
|
|
21
|
+
"""
|
|
22
|
+
settings.FAHD_URL = os.environ.get("FAHD_URL", settings.FAHD_URL)
|
|
23
|
+
settings.FAHD_TENANT_ID = os.environ.get("FAHD_TENANT_ID", settings.FAHD_TENANT_ID)
|
|
24
|
+
settings.FAHD_AUTH_SECRET = os.environ.get("FAHD_AUTH_SECRET", settings.FAHD_AUTH_SECRET)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# fahd-edx-plugin — Django App Plugin for Open edX
|
|
3
|
+
#
|
|
4
|
+
# THE MAIN PACKAGE. Works on ANY edX deployment:
|
|
5
|
+
# Tutor, Kubernetes, native, managed hosting.
|
|
6
|
+
#
|
|
7
|
+
# INSTALL:
|
|
8
|
+
# pip install fahd-edx-plugin
|
|
9
|
+
# # Set env vars: FAHD_URL, FAHD_TENANT_ID, FAHD_AUTH_SECRET
|
|
10
|
+
# # Restart LMS → Fahd appears on every page
|
|
11
|
+
#
|
|
12
|
+
# HOW IT WORKS:
|
|
13
|
+
# Open edX discovers this via lms.djangoapp entry point.
|
|
14
|
+
# apps.py → settings/common.py → appends middleware → done.
|
|
15
|
+
# ============================================================
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[project]
|
|
22
|
+
name = "fahd-edx-plugin"
|
|
23
|
+
version = "1.0.0"
|
|
24
|
+
description = "Fahd AI assistant for Open edX — floating chat on every page"
|
|
25
|
+
requires-python = ">=3.11"
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.entry-points."lms.djangoapp"]
|
|
29
|
+
fahd_edx = "fahd_edx.apps:FahdPluginConfig"
|
|
30
|
+
|
|
31
|
+
[project.entry-points."cms.djangoapp"]
|
|
32
|
+
fahd_edx = "fahd_edx.apps:FahdPluginConfig"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["fahd_edx"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# tutor-fahd — Optional Tutor wrapper for Fahd
|
|
3
|
+
#
|
|
4
|
+
# For Tutor users who want easy config management.
|
|
5
|
+
# The actual plugin logic is in fahd-edx-plugin (separate package).
|
|
6
|
+
#
|
|
7
|
+
# INSTALL:
|
|
8
|
+
# pip install tutor-fahd
|
|
9
|
+
# tutor plugins enable fahd
|
|
10
|
+
# tutor config save --set FAHD_URL=https://fahd.eduarabia.com
|
|
11
|
+
# tutor local launch
|
|
12
|
+
# ============================================================
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["hatchling"]
|
|
16
|
+
build-backend = "hatchling.build"
|
|
17
|
+
|
|
18
|
+
[project]
|
|
19
|
+
name = "tutor-fahd"
|
|
20
|
+
version = "1.0.0"
|
|
21
|
+
description = "Tutor plugin wrapper for Fahd AI assistant"
|
|
22
|
+
requires-python = ">=3.8"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"tutor>=18.0.0,<22.0.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.entry-points."tutor.plugin.v1"]
|
|
28
|
+
fahd = "tutorfahd.plugin"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["tutorfahd"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tutor plugin wrapper for Fahd — manages config and Docker installation."""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# FILE: tutor-fahd/tutorfahd/plugin.py
|
|
3
|
+
# PURPOSE: Thin Tutor wrapper — config management + pip install
|
|
4
|
+
#
|
|
5
|
+
# THIS IS OPTIONAL. The fahd-edx-plugin Django App Plugin works
|
|
6
|
+
# without Tutor. This wrapper just makes it easier for Tutor users:
|
|
7
|
+
# 1. Manages config vars (FAHD_URL, FAHD_TENANT_ID, FAHD_AUTH_SECRET)
|
|
8
|
+
# 2. pip installs fahd-edx-plugin inside the Docker container
|
|
9
|
+
#
|
|
10
|
+
# NO settings patches needed — the Django App Plugin auto-registers
|
|
11
|
+
# itself via lms.djangoapp entry points.
|
|
12
|
+
#
|
|
13
|
+
# PATTERN FROM: cookiecutter-tutor-plugin, tutor-mfe
|
|
14
|
+
#
|
|
15
|
+
# PORTED FROM fahd-agent-v1/plugins/edx/tutor-fahd/tutorfahd/plugin.py.
|
|
16
|
+
# TWO v1->v2 retargets only (both are DEPLOYMENT PLACEHOLDERS — Rule #3,
|
|
17
|
+
# never hardcode a real host/remote; set per deployment):
|
|
18
|
+
# (a) FAHD_URL default -> http://host.docker.internal:8000 (v2 dev host:
|
|
19
|
+
# reaches the host machine's Fahd dev server from inside the edX
|
|
20
|
+
# container).
|
|
21
|
+
# (b) pip git URL -> the v2 repo subdirectory on `tajahdev/fahd-agent`;
|
|
22
|
+
# pin a release tag per deployment.
|
|
23
|
+
# The lms/cms env patches are unchanged.
|
|
24
|
+
# ============================================================
|
|
25
|
+
|
|
26
|
+
from tutor import hooks
|
|
27
|
+
|
|
28
|
+
# ── Config defaults ──────────────────────────────────────────
|
|
29
|
+
# Admins override via: tutor config save --set FAHD_URL=https://...
|
|
30
|
+
|
|
31
|
+
hooks.Filters.CONFIG_DEFAULTS.add_items(
|
|
32
|
+
[
|
|
33
|
+
# v2 dev default: reach the host's Fahd dev server from the container.
|
|
34
|
+
# DEPLOYMENT PLACEHOLDER — set the real URL per deployment (Rule #3).
|
|
35
|
+
("FAHD_URL", "http://host.docker.internal:8000"),
|
|
36
|
+
("FAHD_TENANT_ID", "default"),
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# AUTH_SECRET uses CONFIG_UNIQUE — Tutor auto-generates a random
|
|
41
|
+
# 24-character value. No insecure defaults.
|
|
42
|
+
hooks.Filters.CONFIG_UNIQUE.add_items(
|
|
43
|
+
[
|
|
44
|
+
("FAHD_AUTH_SECRET", "{{ 24|random_string }}"),
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# ── Install fahd-edx-plugin inside the Docker container ──────
|
|
49
|
+
# The Django App Plugin package must exist inside the edX container
|
|
50
|
+
# for auto-discovery to work. This Dockerfile patch adds it.
|
|
51
|
+
#
|
|
52
|
+
# The v2 repo remote is `tajahdev/fahd-agent`; pin a release tag per deployment
|
|
53
|
+
# (never install from main on a live LMS) (Rule #3).
|
|
54
|
+
|
|
55
|
+
hooks.Filters.ENV_PATCHES.add_item(
|
|
56
|
+
(
|
|
57
|
+
"openedx-dockerfile-post-python-requirements",
|
|
58
|
+
'RUN pip install "fahd-edx-plugin @ '
|
|
59
|
+
'git+https://github.com/tajahdev/fahd-agent.git#subdirectory=plugins/edx"',
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# ── Pass config as environment variables ─────────────────────
|
|
64
|
+
# The Django App Plugin reads FAHD_* from env vars (production.py).
|
|
65
|
+
# This patch sets them in the LMS/CMS container environment.
|
|
66
|
+
|
|
67
|
+
hooks.Filters.ENV_PATCHES.add_item(
|
|
68
|
+
(
|
|
69
|
+
"openedx-lms-common-settings",
|
|
70
|
+
"""
|
|
71
|
+
FAHD_URL = "{{ FAHD_URL }}"
|
|
72
|
+
FAHD_TENANT_ID = "{{ FAHD_TENANT_ID }}"
|
|
73
|
+
FAHD_AUTH_SECRET = "{{ FAHD_AUTH_SECRET }}"
|
|
74
|
+
""",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
hooks.Filters.ENV_PATCHES.add_item(
|
|
79
|
+
(
|
|
80
|
+
"openedx-cms-common-settings",
|
|
81
|
+
"""
|
|
82
|
+
FAHD_URL = "{{ FAHD_URL }}"
|
|
83
|
+
FAHD_TENANT_ID = "{{ FAHD_TENANT_ID }}"
|
|
84
|
+
FAHD_AUTH_SECRET = "{{ FAHD_AUTH_SECRET }}"
|
|
85
|
+
""",
|
|
86
|
+
)
|
|
87
|
+
)
|