taleem-kernel 1.2.0 → 1.3.0
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/package.json +2 -2
- package/prisma/dev.db +0 -0
- package/prisma/migrations/20260824182316_add_library_status/migration.sql +24 -0
- package/prisma/schema.prisma +41 -38
- package/readme.md +446 -38
- package/src/ServerKernel.js +2 -2
- package/src/modules/Admin.js +73 -11
- package/src/modules/User.js +50 -7
- package/src/utils/JWT.js +31 -0
- package/src/Auth.js +0 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taleem-kernel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Taleem data and business-logic kernel. HTTP is only an adapter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/ServerKernel.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"taleem-kernel": "./bin/cli.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "vitest run"
|
|
14
|
+
"test": "vitest run --no-file-parallelism"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"src",
|
package/prisma/dev.db
CHANGED
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- RedefineTables
|
|
2
|
+
PRAGMA defer_foreign_keys=ON;
|
|
3
|
+
PRAGMA foreign_keys=OFF;
|
|
4
|
+
CREATE TABLE "new_Library" (
|
|
5
|
+
"slug" TEXT NOT NULL PRIMARY KEY,
|
|
6
|
+
"title" TEXT NOT NULL,
|
|
7
|
+
"description" TEXT,
|
|
8
|
+
"thumbnail" TEXT,
|
|
9
|
+
"type" TEXT NOT NULL,
|
|
10
|
+
"status" TEXT NOT NULL DEFAULT 'DRAFT',
|
|
11
|
+
"body" TEXT,
|
|
12
|
+
"courseSlug" TEXT NOT NULL,
|
|
13
|
+
"groupSlug" TEXT NOT NULL,
|
|
14
|
+
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
"allowCommunication" BOOLEAN NOT NULL DEFAULT true,
|
|
16
|
+
"meta" TEXT,
|
|
17
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
18
|
+
"updatedAt" DATETIME NOT NULL
|
|
19
|
+
);
|
|
20
|
+
INSERT INTO "new_Library" ("allowCommunication", "body", "courseSlug", "createdAt", "description", "groupSlug", "meta", "slug", "sortOrder", "thumbnail", "title", "type", "updatedAt") SELECT "allowCommunication", "body", "courseSlug", "createdAt", "description", "groupSlug", "meta", "slug", "sortOrder", "thumbnail", "title", "type", "updatedAt" FROM "Library";
|
|
21
|
+
DROP TABLE "Library";
|
|
22
|
+
ALTER TABLE "new_Library" RENAME TO "Library";
|
|
23
|
+
PRAGMA foreign_keys=ON;
|
|
24
|
+
PRAGMA defer_foreign_keys=OFF;
|
package/prisma/schema.prisma
CHANGED
|
@@ -13,6 +13,12 @@ enum ContentType {
|
|
|
13
13
|
MCQ
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
enum LibraryStatus {
|
|
17
|
+
DRAFT
|
|
18
|
+
PUBLISHED
|
|
19
|
+
ARCHIVED
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
enum CourseAccess {
|
|
17
23
|
OPEN
|
|
18
24
|
MEMBERS
|
|
@@ -25,12 +31,12 @@ enum AdminRole {
|
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
model Group {
|
|
28
|
-
id
|
|
29
|
-
courseSlug
|
|
30
|
-
slug
|
|
31
|
-
title
|
|
32
|
-
thumbnail
|
|
33
|
-
items
|
|
34
|
+
id Int @id @default(autoincrement())
|
|
35
|
+
courseSlug String
|
|
36
|
+
slug String
|
|
37
|
+
title String
|
|
38
|
+
thumbnail String?
|
|
39
|
+
items Json
|
|
34
40
|
|
|
35
41
|
@@unique([courseSlug, slug])
|
|
36
42
|
@@index([courseSlug])
|
|
@@ -46,15 +52,15 @@ model Course {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
model User {
|
|
49
|
-
id
|
|
50
|
-
email
|
|
51
|
-
password
|
|
52
|
-
name
|
|
53
|
-
role
|
|
54
|
-
resource
|
|
55
|
-
createdAt
|
|
56
|
-
subscriptions
|
|
57
|
-
communications
|
|
55
|
+
id Int @id @default(autoincrement())
|
|
56
|
+
email String @unique
|
|
57
|
+
password String
|
|
58
|
+
name String?
|
|
59
|
+
role String @default("student")
|
|
60
|
+
resource String?
|
|
61
|
+
createdAt DateTime @default(now())
|
|
62
|
+
subscriptions Subscription[]
|
|
63
|
+
communications Communication[]
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
model Admin {
|
|
@@ -69,19 +75,20 @@ model Admin {
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
model Library {
|
|
72
|
-
slug String
|
|
78
|
+
slug String @id
|
|
73
79
|
title String
|
|
74
80
|
description String?
|
|
75
81
|
thumbnail String?
|
|
76
82
|
type ContentType
|
|
83
|
+
status LibraryStatus @default(DRAFT)
|
|
77
84
|
body String?
|
|
78
85
|
courseSlug String
|
|
79
86
|
groupSlug String
|
|
80
|
-
sortOrder Int
|
|
81
|
-
allowCommunication Boolean
|
|
87
|
+
sortOrder Int @default(0)
|
|
88
|
+
allowCommunication Boolean @default(true)
|
|
82
89
|
meta String?
|
|
83
|
-
createdAt DateTime
|
|
84
|
-
updatedAt DateTime
|
|
90
|
+
createdAt DateTime @default(now())
|
|
91
|
+
updatedAt DateTime @updatedAt
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
model Subscription {
|
|
@@ -110,50 +117,46 @@ model Communication {
|
|
|
110
117
|
readAt DateTime?
|
|
111
118
|
createdAt DateTime @default(now())
|
|
112
119
|
updatedAt DateTime @updatedAt
|
|
113
|
-
user User
|
|
120
|
+
user User @relation(fields: [userId], references: [id])
|
|
114
121
|
|
|
115
122
|
@@index([userId])
|
|
116
123
|
@@index([librarySlug])
|
|
117
124
|
}
|
|
118
|
-
model Svg {
|
|
119
125
|
|
|
120
|
-
|
|
126
|
+
model Svg {
|
|
127
|
+
slug String @id
|
|
121
128
|
|
|
122
|
-
title
|
|
129
|
+
title String
|
|
123
130
|
|
|
124
|
-
body
|
|
131
|
+
body String
|
|
125
132
|
|
|
126
|
-
tags
|
|
133
|
+
tags String @default("[]")
|
|
127
134
|
|
|
128
135
|
createdAt DateTime @default(now())
|
|
129
136
|
|
|
130
137
|
updatedAt DateTime @updatedAt
|
|
131
|
-
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
model Image {
|
|
141
|
+
slug String @id
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
title String
|
|
143
|
+
title String
|
|
139
144
|
|
|
140
|
-
tags
|
|
145
|
+
tags String @default("[]")
|
|
141
146
|
|
|
142
147
|
createdAt DateTime @default(now())
|
|
143
148
|
|
|
144
149
|
updatedAt DateTime @updatedAt
|
|
145
|
-
|
|
146
150
|
}
|
|
147
|
-
model Audio {
|
|
148
151
|
|
|
149
|
-
|
|
152
|
+
model Audio {
|
|
153
|
+
slug String @id
|
|
150
154
|
|
|
151
|
-
title
|
|
155
|
+
title String
|
|
152
156
|
|
|
153
|
-
tags
|
|
157
|
+
tags String @default("[]")
|
|
154
158
|
|
|
155
159
|
createdAt DateTime @default(now())
|
|
156
160
|
|
|
157
161
|
updatedAt DateTime @updatedAt
|
|
158
|
-
|
|
159
|
-
}
|
|
162
|
+
}
|
package/readme.md
CHANGED
|
@@ -1,97 +1,505 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# Taleem Kernel API
|
|
2
4
|
|
|
3
5
|
```js
|
|
4
6
|
import kernel from "taleem-kernel";
|
|
5
|
-
|
|
7
|
+
````
|
|
8
|
+
|
|
9
|
+
`taleem-kernel` is the shared DB/domain layer for Taleem applications.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
It owns the canonical Taleem Prisma/SQLite schema and provides the domain modules used by Taleem Server, Taleem Studio, and other Taleem applications.
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
The kernel is intentionally small: modules expose operations that make sense for Taleem rather than blindly exposing Prisma CRUD.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core
|
|
10
18
|
|
|
11
19
|
```js
|
|
12
|
-
kernel.db
|
|
13
|
-
|
|
14
|
-
kernel.
|
|
20
|
+
kernel.db
|
|
21
|
+
|
|
22
|
+
kernel.config
|
|
23
|
+
|
|
24
|
+
kernel.jwt
|
|
25
|
+
|
|
15
26
|
kernel.communicationPolicy
|
|
27
|
+
|
|
16
28
|
kernel.shutdown()
|
|
17
29
|
```
|
|
18
30
|
|
|
19
|
-
|
|
31
|
+
`kernel.jwt` is the low-level JWT utility used by the User and Admin modules.
|
|
32
|
+
|
|
33
|
+
Applications normally should not create or verify application identity tokens directly through `kernel.jwt`. Instead, use the authentication methods on the appropriate domain module.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Modules
|
|
20
38
|
|
|
21
39
|
```js
|
|
22
40
|
kernel.user
|
|
41
|
+
|
|
23
42
|
kernel.admin
|
|
43
|
+
|
|
24
44
|
kernel.course
|
|
45
|
+
|
|
25
46
|
kernel.library
|
|
47
|
+
|
|
26
48
|
kernel.communication
|
|
49
|
+
|
|
27
50
|
kernel.subscription
|
|
51
|
+
|
|
28
52
|
kernel.svg
|
|
53
|
+
|
|
29
54
|
kernel.image
|
|
55
|
+
|
|
30
56
|
kernel.audio
|
|
31
57
|
```
|
|
32
58
|
|
|
33
|
-
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## User
|
|
62
|
+
|
|
63
|
+
Users are the normal Taleem application users.
|
|
64
|
+
|
|
65
|
+
User registration and authentication are owned directly by the User module.
|
|
66
|
+
|
|
67
|
+
### User API
|
|
34
68
|
|
|
35
69
|
```js
|
|
36
|
-
list()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
kernel.user.list()
|
|
71
|
+
|
|
72
|
+
kernel.user.get(id)
|
|
73
|
+
|
|
74
|
+
kernel.user.getByEmail(email)
|
|
75
|
+
|
|
76
|
+
kernel.user.emailToId(email)
|
|
77
|
+
|
|
78
|
+
kernel.user.register(data)
|
|
79
|
+
|
|
80
|
+
kernel.user.login(email, password)
|
|
81
|
+
|
|
82
|
+
kernel.user.createToken(user)
|
|
83
|
+
|
|
84
|
+
kernel.user.authenticate(token)
|
|
85
|
+
|
|
86
|
+
kernel.user.update(id, data)
|
|
87
|
+
|
|
88
|
+
kernel.user.delete(id)
|
|
41
89
|
```
|
|
42
90
|
|
|
43
|
-
|
|
91
|
+
Registration creates a User and hashes the supplied password:
|
|
44
92
|
|
|
45
93
|
```js
|
|
46
|
-
kernel.user.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
kernel.communication.listUnanswered()
|
|
51
|
-
kernel.communication.countUserOpenQuestions()
|
|
94
|
+
const user = await kernel.user.register({
|
|
95
|
+
email: "student@example.com",
|
|
96
|
+
password: "12345678"
|
|
97
|
+
});
|
|
52
98
|
```
|
|
53
99
|
|
|
54
|
-
|
|
100
|
+
Login returns a User JWT:
|
|
55
101
|
|
|
56
|
-
|
|
102
|
+
```js
|
|
103
|
+
const token = await kernel.user.login(
|
|
104
|
+
"student@example.com",
|
|
105
|
+
"12345678"
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The token can then be authenticated:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const user = await kernel.user.authenticate(token);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
User authentication accepts only tokens with:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
{
|
|
119
|
+
type: "user"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The User module creates its own tokens using the kernel JWT utility.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Admin
|
|
128
|
+
|
|
129
|
+
Admins are independent of Course structure.
|
|
130
|
+
|
|
131
|
+
An Admin may be assigned one or more course slugs through `courseSlugs`.
|
|
132
|
+
|
|
133
|
+
Admin accounts are provisioned separately from normal User registration.
|
|
134
|
+
|
|
135
|
+
### Admin API
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
kernel.admin.list()
|
|
139
|
+
|
|
140
|
+
kernel.admin.get(email)
|
|
141
|
+
|
|
142
|
+
kernel.admin.login(email, password)
|
|
143
|
+
|
|
144
|
+
kernel.admin.createToken(admin)
|
|
145
|
+
|
|
146
|
+
kernel.admin.authenticate(token)
|
|
147
|
+
|
|
148
|
+
kernel.admin.create(data)
|
|
149
|
+
|
|
150
|
+
kernel.admin.update(email, data)
|
|
151
|
+
|
|
152
|
+
kernel.admin.delete(email)
|
|
153
|
+
|
|
154
|
+
kernel.admin.isAdmin(email, courseSlug)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Admin login returns an Admin JWT:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const token = await kernel.admin.login(
|
|
161
|
+
"admin@example.com",
|
|
162
|
+
"12345678"
|
|
163
|
+
);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The token can then be authenticated:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
const admin = await kernel.admin.authenticate(token);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Admin authentication accepts only tokens with:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
{
|
|
176
|
+
type: "admin"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Admin authorization is handled separately:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
const allowed = await kernel.admin.isAdmin(
|
|
184
|
+
admin.email,
|
|
185
|
+
courseSlug
|
|
186
|
+
);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Authentication and authorization are separate concerns.
|
|
190
|
+
|
|
191
|
+
An authenticated Admin is not automatically authorized for every Course.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## JWT
|
|
196
|
+
|
|
197
|
+
JWT is the low-level token utility used by the User and Admin modules.
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
kernel.jwt.sign(payload)
|
|
201
|
+
|
|
202
|
+
kernel.jwt.verify(token)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
User and Admin own their respective identity tokens.
|
|
206
|
+
|
|
207
|
+
Applications should normally use:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
kernel.user.login(...)
|
|
211
|
+
kernel.user.authenticate(...)
|
|
212
|
+
|
|
213
|
+
kernel.admin.login(...)
|
|
214
|
+
kernel.admin.authenticate(...)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
rather than calling `kernel.jwt` directly.
|
|
218
|
+
|
|
219
|
+
The important architectural rule is:
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
JWT
|
|
223
|
+
↓
|
|
224
|
+
├── User
|
|
225
|
+
│ ├── login()
|
|
226
|
+
│ ├── createToken()
|
|
227
|
+
│ └── authenticate()
|
|
228
|
+
│
|
|
229
|
+
└── Admin
|
|
230
|
+
├── login()
|
|
231
|
+
├── createToken()
|
|
232
|
+
└── authenticate()
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
There is no shared identity/authentication module routing between User and Admin.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Course
|
|
240
|
+
|
|
241
|
+
Course is **not a CRUD entity**.
|
|
242
|
+
|
|
243
|
+
A Course originates as a static course artifact (`course.json`). The artifact is the source of truth and is materialized into the database by the kernel.
|
|
244
|
+
|
|
245
|
+
Internally, a Course is represented by two tables:
|
|
246
|
+
|
|
247
|
+
```text
|
|
248
|
+
Course
|
|
249
|
+
|
|
|
250
|
+
+-- Group
|
|
251
|
+
|
|
|
252
|
+
+-- items[] // library slugs
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
There are intentionally no Prisma relationships between Course, Group, and Library. Their relationship is maintained through Taleem slugs.
|
|
256
|
+
|
|
257
|
+
### Course API
|
|
57
258
|
|
|
58
259
|
```js
|
|
59
|
-
kernel.auth.authenticate(token)
|
|
60
|
-
kernel.user.login(email, password)
|
|
61
260
|
kernel.course.list()
|
|
261
|
+
|
|
262
|
+
kernel.course.get(courseSlug)
|
|
263
|
+
|
|
264
|
+
kernel.course.getGroups(courseSlug)
|
|
265
|
+
|
|
266
|
+
kernel.course.getGroup(courseSlug, groupSlug)
|
|
267
|
+
|
|
268
|
+
kernel.course.getGroupItems(courseSlug, groupSlug)
|
|
269
|
+
|
|
270
|
+
kernel.course.seed(course)
|
|
271
|
+
|
|
272
|
+
kernel.course.wipe()
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`seed()` materializes a complete course artifact:
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
await kernel.course.seed(course);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`wipe()` removes the materialized Course and Group data before rebuilding from the source artifacts:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
await kernel.course.wipe();
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
There are deliberately no:
|
|
288
|
+
|
|
289
|
+
```js
|
|
290
|
+
course.create()
|
|
291
|
+
|
|
292
|
+
course.update()
|
|
293
|
+
|
|
294
|
+
course.delete()
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
because a Course is an artifact, not an independently edited CRUD record.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Library
|
|
302
|
+
|
|
303
|
+
Library contains the actual authored Taleem content.
|
|
304
|
+
|
|
305
|
+
Library items belong logically to a Course and Group through string slugs:
|
|
306
|
+
|
|
307
|
+
```text
|
|
308
|
+
Library.courseSlug → Course.slug
|
|
309
|
+
|
|
310
|
+
Library.groupSlug → Group.slug
|
|
311
|
+
|
|
312
|
+
Library.slug → Group.items[]
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
These are application-level relationships, not Prisma foreign keys.
|
|
316
|
+
|
|
317
|
+
### Library API
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
kernel.library.list()
|
|
321
|
+
|
|
322
|
+
kernel.library.listByCourse(courseSlug)
|
|
323
|
+
|
|
324
|
+
kernel.library.listByGroup(courseSlug, groupSlug)
|
|
325
|
+
|
|
62
326
|
kernel.library.get(slug)
|
|
63
|
-
|
|
327
|
+
|
|
328
|
+
kernel.library.create(data)
|
|
329
|
+
|
|
330
|
+
kernel.library.update(slug, data)
|
|
331
|
+
|
|
332
|
+
kernel.library.delete(slug)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Filtering:
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
kernel.library.list({
|
|
339
|
+
type,
|
|
340
|
+
courseSlug,
|
|
341
|
+
groupSlug
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`list()` returns the library metadata needed by applications without requiring the full content body.
|
|
346
|
+
|
|
347
|
+
Library items also have a lifecycle status:
|
|
348
|
+
|
|
349
|
+
```text
|
|
350
|
+
DRAFT
|
|
351
|
+
PUBLISHED
|
|
352
|
+
ARCHIVED
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The default status for a new Library item is:
|
|
356
|
+
|
|
357
|
+
```text
|
|
358
|
+
DRAFT
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Assets
|
|
364
|
+
|
|
365
|
+
SVG, Image, and Audio are independent reusable assets.
|
|
366
|
+
|
|
367
|
+
They are not tied to Courses, Groups, or Library records at the database level.
|
|
368
|
+
|
|
369
|
+
Their metadata is intentionally small:
|
|
370
|
+
|
|
371
|
+
```text
|
|
372
|
+
slug
|
|
373
|
+
|
|
374
|
+
title
|
|
375
|
+
|
|
376
|
+
tags
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Assets can be discovered through tags and referenced by content where appropriate.
|
|
380
|
+
|
|
381
|
+
```js
|
|
382
|
+
kernel.svg
|
|
383
|
+
|
|
384
|
+
kernel.image
|
|
385
|
+
|
|
386
|
+
kernel.audio
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
These modules provide the normal asset operations such as:
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
list()
|
|
393
|
+
|
|
394
|
+
get()
|
|
395
|
+
|
|
396
|
+
create()
|
|
397
|
+
|
|
398
|
+
update()
|
|
399
|
+
|
|
400
|
+
delete()
|
|
64
401
|
```
|
|
65
402
|
|
|
66
|
-
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Schema
|
|
406
|
+
|
|
407
|
+
The kernel owns the **canonical Taleem Prisma/SQLite schema**.
|
|
408
|
+
|
|
409
|
+
Applications using the Taleem database should use the kernel rather than maintaining their own DB/domain layer.
|
|
410
|
+
|
|
411
|
+
The important architectural rule is:
|
|
412
|
+
|
|
413
|
+
```text
|
|
414
|
+
Kernel
|
|
415
|
+
|
|
416
|
+
↓
|
|
417
|
+
|
|
418
|
+
Canonical schema + Taleem domain operations
|
|
419
|
+
|
|
420
|
+
↓
|
|
421
|
+
|
|
422
|
+
Applications
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Applications should not bypass the kernel for normal Taleem DB operations.
|
|
426
|
+
|
|
427
|
+
---
|
|
67
428
|
|
|
68
429
|
## Schema Management
|
|
69
430
|
|
|
70
|
-
The kernel provides
|
|
431
|
+
The kernel provides CLI commands for maintaining schema compatibility:
|
|
71
432
|
|
|
72
433
|
```bash
|
|
73
434
|
npx taleem-kernel schema-check
|
|
435
|
+
|
|
74
436
|
npx taleem-kernel schema-update
|
|
75
437
|
```
|
|
76
438
|
|
|
77
|
-
`schema-check` compares the application's
|
|
439
|
+
`schema-check` compares the application's Prisma schema with the canonical schema shipped with `taleem-kernel`.
|
|
78
440
|
|
|
79
|
-
`schema-update` copies the kernel's canonical schema into the application's
|
|
441
|
+
`schema-update` copies the kernel's canonical schema into the application's Prisma schema.
|
|
80
442
|
|
|
81
443
|
After updating the schema, run the application's normal Prisma migration/generation process as required.
|
|
82
444
|
|
|
445
|
+
---
|
|
446
|
+
|
|
83
447
|
## Server Dependencies
|
|
84
448
|
|
|
85
449
|
`taleem-server` should use `taleem-kernel` for all Taleem database access.
|
|
86
450
|
|
|
87
|
-
The server provides the HTTP/API layer on top of the kernel
|
|
451
|
+
The server provides the HTTP/API layer on top of the kernel.
|
|
88
452
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
453
|
+
The server should **not maintain its own Taleem Prisma models or duplicate DB modules**.
|
|
454
|
+
|
|
455
|
+
Authentication at the server layer should use the appropriate Kernel identity module:
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
const user = await kernel.user.authenticate(token);
|
|
95
459
|
```
|
|
96
460
|
|
|
97
|
-
|
|
461
|
+
or:
|
|
462
|
+
|
|
463
|
+
```js
|
|
464
|
+
const admin = await kernel.admin.authenticate(token);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Course-level Admin authorization can then be checked separately:
|
|
468
|
+
|
|
469
|
+
```js
|
|
470
|
+
const allowed = await kernel.admin.isAdmin(
|
|
471
|
+
admin.email,
|
|
472
|
+
courseSlug
|
|
473
|
+
);
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Design Principles
|
|
479
|
+
|
|
480
|
+
The kernel follows a few important Taleem-specific rules:
|
|
481
|
+
|
|
482
|
+
1. **Course artifacts are the source of truth.**
|
|
483
|
+
|
|
484
|
+
2. **Course is not generic CRUD.** It is materialized from the course artifact.
|
|
485
|
+
|
|
486
|
+
3. **Course and Group are internally separate tables but one domain concept.**
|
|
487
|
+
|
|
488
|
+
4. **Course, Group, and Library are connected by slugs, not Prisma relationships.**
|
|
489
|
+
|
|
490
|
+
5. **Relational integrity is enforced by the Taleem build/application layer.**
|
|
491
|
+
|
|
492
|
+
6. **Library contains authored content; Course defines the expected structure and item slugs.**
|
|
493
|
+
|
|
494
|
+
7. **Library content has an explicit lifecycle: DRAFT, PUBLISHED, and ARCHIVED.**
|
|
495
|
+
|
|
496
|
+
8. **SVG, Image, and Audio are independent reusable assets.**
|
|
497
|
+
|
|
498
|
+
9. **User and Admin authentication are separate domain concerns.**
|
|
499
|
+
|
|
500
|
+
10. **JWT is a low-level utility; User and Admin own their identity token lifecycle.**
|
|
501
|
+
|
|
502
|
+
11. **The kernel exposes domain operations only where they represent real Taleem behavior.**
|
|
503
|
+
|
|
504
|
+
The goal is a small, stable kernel that owns the Taleem contract without pretending to be a generic CRUD framework.
|
|
505
|
+
|
package/src/ServerKernel.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { PrismaClient } from "@prisma/client";
|
|
4
4
|
import Config from "./Config.js";
|
|
5
|
-
import Auth from "./Auth.js";
|
|
5
|
+
// import Auth from "./Auth.js";
|
|
6
6
|
import CommunicationPolicy from "./CommunicationPolicy.js";
|
|
7
7
|
import User from "./modules/User.js";
|
|
8
8
|
import Admin from "./modules/Admin.js";
|
|
@@ -18,7 +18,7 @@ class ServerKernel {
|
|
|
18
18
|
constructor() {
|
|
19
19
|
this.config = new Config();
|
|
20
20
|
this.db = new PrismaClient();
|
|
21
|
-
this.auth = new Auth(this);
|
|
21
|
+
// this.auth = new Auth(this);
|
|
22
22
|
this.communicationPolicy = new CommunicationPolicy(this);
|
|
23
23
|
this.user = new User(this);
|
|
24
24
|
this.admin = new Admin(this);
|
package/src/modules/Admin.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// /home/bilal-tariq/00--TALEEM/taleem-kernel/src/modules/Admin.js
|
|
2
2
|
|
|
3
3
|
import bcrypt from "bcrypt";
|
|
4
|
+
import JWT from "../utils/JWT.js";
|
|
4
5
|
|
|
5
6
|
export default class Admin {
|
|
6
|
-
constructor(kernel) {
|
|
7
|
+
constructor(kernel) {
|
|
8
|
+
this.kernel = kernel;
|
|
9
|
+
this.jwt = new JWT(kernel);
|
|
10
|
+
}
|
|
7
11
|
|
|
8
12
|
async list(filters = {}) {
|
|
9
13
|
const where = {};
|
|
@@ -15,37 +19,95 @@ export default class Admin {
|
|
|
15
19
|
return this.kernel.db.admin.findUnique({ where: { email } });
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
// Authentication
|
|
23
|
+
|
|
18
24
|
async login(email, password) {
|
|
19
25
|
const admin = await this.get(email);
|
|
20
|
-
|
|
21
|
-
if (!admin
|
|
26
|
+
|
|
27
|
+
if (!admin) {
|
|
28
|
+
throw new Error(`Admin.login(): Admin '${email}' not found.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!admin.isActive) {
|
|
32
|
+
throw new Error(`Admin.login(): Admin '${email}' is inactive.`);
|
|
33
|
+
}
|
|
22
34
|
|
|
23
35
|
const ok = await bcrypt.compare(password, admin.password);
|
|
24
|
-
if (!ok) throw new Error(`Admin.login(): Invalid password.`);
|
|
25
36
|
|
|
26
|
-
|
|
37
|
+
if (!ok) {
|
|
38
|
+
throw new Error(`Admin.login(): Invalid password.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.createToken(admin);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async createToken(admin) {
|
|
45
|
+
return this.jwt.sign({
|
|
46
|
+
email: admin.email,
|
|
47
|
+
type: "admin"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async authenticate(token) {
|
|
52
|
+
const payload = this.jwt.verify(token);
|
|
53
|
+
|
|
54
|
+
if (payload.type !== "admin") {
|
|
55
|
+
throw new Error("Admin token required.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const admin = await this.get(payload.email);
|
|
59
|
+
|
|
60
|
+
if (!admin) {
|
|
61
|
+
throw new Error("Admin not found.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!admin.isActive) {
|
|
65
|
+
throw new Error("Admin is inactive.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return admin;
|
|
27
69
|
}
|
|
28
70
|
|
|
71
|
+
// CRUD
|
|
72
|
+
|
|
29
73
|
async create(data) {
|
|
30
74
|
const createData = { ...data };
|
|
31
|
-
|
|
32
|
-
|
|
75
|
+
|
|
76
|
+
if (createData.password) {
|
|
77
|
+
createData.password = await bcrypt.hash(createData.password, 10);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return this.kernel.db.admin.create({
|
|
81
|
+
data: createData
|
|
82
|
+
});
|
|
33
83
|
}
|
|
34
84
|
|
|
35
85
|
async update(email, data) {
|
|
36
86
|
const updateData = { ...data };
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
|
|
88
|
+
if (updateData.password) {
|
|
89
|
+
updateData.password = await bcrypt.hash(updateData.password, 10);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.kernel.db.admin.update({
|
|
93
|
+
where: { email },
|
|
94
|
+
data: updateData
|
|
95
|
+
});
|
|
39
96
|
}
|
|
40
97
|
|
|
41
98
|
async delete(email) {
|
|
42
|
-
return this.kernel.db.admin.delete({
|
|
99
|
+
return this.kernel.db.admin.delete({
|
|
100
|
+
where: { email }
|
|
101
|
+
});
|
|
43
102
|
}
|
|
44
103
|
|
|
45
104
|
async isAdmin(email, courseSlug) {
|
|
46
105
|
const admin = await this.get(email);
|
|
106
|
+
|
|
47
107
|
if (!admin) return false;
|
|
108
|
+
|
|
48
109
|
const courseSlugs = JSON.parse(admin.courseSlugs || "[]");
|
|
110
|
+
|
|
49
111
|
return courseSlugs.includes(courseSlug);
|
|
50
112
|
}
|
|
51
113
|
}
|
package/src/modules/User.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// src/serverKernel/modules/User.js
|
|
2
2
|
|
|
3
3
|
import bcrypt from "bcrypt";
|
|
4
|
+
import JWT from "../utils/JWT.js";
|
|
4
5
|
|
|
5
6
|
export default class User {
|
|
6
7
|
constructor(kernel) {
|
|
7
8
|
this.kernel = kernel;
|
|
9
|
+
this.jwt = new JWT(kernel);
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
// Queries
|
|
@@ -31,28 +33,69 @@ export default class User {
|
|
|
31
33
|
|
|
32
34
|
async register(data) {
|
|
33
35
|
const password = await bcrypt.hash(data.password, 10);
|
|
34
|
-
|
|
36
|
+
|
|
37
|
+
return this.kernel.db.user.create({
|
|
38
|
+
data: { ...data, password }
|
|
39
|
+
});
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
async login(email, password) {
|
|
38
43
|
const user = await this.getByEmail(email);
|
|
39
|
-
|
|
44
|
+
|
|
45
|
+
if (!user) {
|
|
46
|
+
throw new Error(`User.login(): User '${email}' not found.`);
|
|
47
|
+
}
|
|
40
48
|
|
|
41
49
|
const ok = await bcrypt.compare(password, user.password);
|
|
42
|
-
if (!ok) throw new Error(`User.login(): Invalid password.`);
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
if (!ok) {
|
|
52
|
+
throw new Error(`User.login(): Invalid password.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return this.createToken(user);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async createToken(user) {
|
|
59
|
+
return this.jwt.sign({
|
|
60
|
+
id: user.id,
|
|
61
|
+
type: "user"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async authenticate(token) {
|
|
66
|
+
const payload = this.jwt.verify(token);
|
|
67
|
+
|
|
68
|
+
if (payload.type !== "user") {
|
|
69
|
+
throw new Error("User token required.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const user = await this.get(payload.id);
|
|
73
|
+
|
|
74
|
+
if (!user) {
|
|
75
|
+
throw new Error("User not found.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return user;
|
|
45
79
|
}
|
|
46
80
|
|
|
47
81
|
// CRUD
|
|
48
82
|
|
|
49
83
|
async update(id, data) {
|
|
50
84
|
const updateData = { ...data };
|
|
51
|
-
|
|
52
|
-
|
|
85
|
+
|
|
86
|
+
if (updateData.password) {
|
|
87
|
+
updateData.password = await bcrypt.hash(updateData.password, 10);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.kernel.db.user.update({
|
|
91
|
+
where: { id },
|
|
92
|
+
data: updateData
|
|
93
|
+
});
|
|
53
94
|
}
|
|
54
95
|
|
|
55
96
|
async delete(id) {
|
|
56
|
-
return this.kernel.db.user.delete({
|
|
97
|
+
return this.kernel.db.user.delete({
|
|
98
|
+
where: { id }
|
|
99
|
+
});
|
|
57
100
|
}
|
|
58
101
|
}
|
package/src/utils/JWT.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// /home/bilal-tariq/00--TALEEM/taleem-kernel/src/utils/JWT.js
|
|
2
|
+
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
|
|
5
|
+
export default class JWT {
|
|
6
|
+
|
|
7
|
+
constructor(kernel) {
|
|
8
|
+
|
|
9
|
+
this.kernel = kernel;
|
|
10
|
+
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
sign(payload) {
|
|
14
|
+
|
|
15
|
+
return jwt.sign(
|
|
16
|
+
payload,
|
|
17
|
+
this.kernel.config.jwtSecret
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
verify(token) {
|
|
23
|
+
|
|
24
|
+
return jwt.verify(
|
|
25
|
+
token,
|
|
26
|
+
this.kernel.config.jwtSecret
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
}
|
package/src/Auth.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
///home/bilal-tariq/00--TALEEM/taleem-server/src/serverKernel/Auth.js
|
|
2
|
-
|
|
3
|
-
import JWT from "./JWT.js";
|
|
4
|
-
|
|
5
|
-
export default class Auth {
|
|
6
|
-
|
|
7
|
-
constructor(kernel) {
|
|
8
|
-
this.kernel = kernel;
|
|
9
|
-
this.jwt = new JWT(kernel);
|
|
10
|
-
}
|
|
11
|
-
// --------------------------------------------------
|
|
12
|
-
// Token Creation
|
|
13
|
-
// --------------------------------------------------
|
|
14
|
-
createUserToken(user) {
|
|
15
|
-
|
|
16
|
-
return this.jwt.sign({ id: user.id, type: "user" });
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
createAdminToken(admin) {
|
|
21
|
-
|
|
22
|
-
return this.jwt.sign({ id: admin.id, type: "admin" });
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// --------------------------------------------------
|
|
27
|
-
// Authentication
|
|
28
|
-
// --------------------------------------------------
|
|
29
|
-
|
|
30
|
-
async authenticate(token) {
|
|
31
|
-
|
|
32
|
-
const { id, type } = this.verifyToken(token);
|
|
33
|
-
|
|
34
|
-
if (type === "user") return this.authenticateUser(id);
|
|
35
|
-
|
|
36
|
-
if (type === "admin") return this.authenticateAdmin(id);
|
|
37
|
-
|
|
38
|
-
this.fail("authenticate()", `Unknown identity type '${type}'.`);
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async authenticateUser(id) {
|
|
43
|
-
|
|
44
|
-
const user = await this.kernel.user.get(id);
|
|
45
|
-
|
|
46
|
-
if (!user)
|
|
47
|
-
this.fail(
|
|
48
|
-
"authenticateUser()",
|
|
49
|
-
`User '${id}' does not exist.`
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
return user;
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async authenticateAdmin(id) {
|
|
57
|
-
|
|
58
|
-
const admin = await this.kernel.admin.get(id);
|
|
59
|
-
|
|
60
|
-
if (!admin)
|
|
61
|
-
this.fail(
|
|
62
|
-
"authenticateAdmin()",
|
|
63
|
-
`Admin '${id}' does not exist.`
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
return admin;
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
verifyToken(token) {
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
|
|
74
|
-
return this.jwt.verify(token);
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
|
|
79
|
-
this.fail("verifyToken()", error.message);
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// --------------------------------------------------
|
|
86
|
-
// Helpers
|
|
87
|
-
// --------------------------------------------------
|
|
88
|
-
|
|
89
|
-
fail(method, reason) {
|
|
90
|
-
|
|
91
|
-
throw new Error(
|
|
92
|
-
[
|
|
93
|
-
"",
|
|
94
|
-
"========================================",
|
|
95
|
-
"AUTHENTICATION FAILED",
|
|
96
|
-
"----------------------------------------",
|
|
97
|
-
`Method : Auth.${method}`,
|
|
98
|
-
`Reason : ${reason}`,
|
|
99
|
-
"========================================"
|
|
100
|
-
].join("\n")
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
}
|