taleem-kernel 1.0.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taleem-kernel",
3
- "version": "1.0.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,39 @@
1
+ /*
2
+ Warnings:
3
+
4
+ - You are about to drop the column `groupings` on the `Course` table. All the data in the column will be lost.
5
+
6
+ */
7
+ -- CreateTable
8
+ CREATE TABLE "Group" (
9
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
10
+ "courseSlug" TEXT NOT NULL,
11
+ "slug" TEXT NOT NULL,
12
+ "title" TEXT NOT NULL,
13
+ "thumbnail" TEXT,
14
+ "items" JSONB NOT NULL
15
+ );
16
+
17
+ -- RedefineTables
18
+ PRAGMA defer_foreign_keys=ON;
19
+ PRAGMA foreign_keys=OFF;
20
+ CREATE TABLE "new_Course" (
21
+ "slug" TEXT NOT NULL,
22
+ "title" TEXT NOT NULL,
23
+ "description" TEXT,
24
+ "thumbnail" TEXT,
25
+ "access" TEXT NOT NULL DEFAULT 'OPEN',
26
+ "price" INTEGER NOT NULL DEFAULT 0
27
+ );
28
+ INSERT INTO "new_Course" ("access", "description", "price", "slug", "thumbnail", "title") SELECT "access", "description", "price", "slug", "thumbnail", "title" FROM "Course";
29
+ DROP TABLE "Course";
30
+ ALTER TABLE "new_Course" RENAME TO "Course";
31
+ CREATE UNIQUE INDEX "Course_slug_key" ON "Course"("slug");
32
+ PRAGMA foreign_keys=ON;
33
+ PRAGMA defer_foreign_keys=OFF;
34
+
35
+ -- CreateIndex
36
+ CREATE INDEX "Group_courseSlug_idx" ON "Group"("courseSlug");
37
+
38
+ -- CreateIndex
39
+ CREATE UNIQUE INDEX "Group_courseSlug_slug_key" ON "Group"("courseSlug", "slug");
@@ -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;
@@ -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
@@ -24,6 +30,18 @@ enum AdminRole {
24
30
  SUPER_ADMIN
25
31
  }
26
32
 
33
+ model Group {
34
+ id Int @id @default(autoincrement())
35
+ courseSlug String
36
+ slug String
37
+ title String
38
+ thumbnail String?
39
+ items Json
40
+
41
+ @@unique([courseSlug, slug])
42
+ @@index([courseSlug])
43
+ }
44
+
27
45
  model Course {
28
46
  slug String @unique
29
47
  title String
@@ -31,19 +49,18 @@ model Course {
31
49
  thumbnail String?
32
50
  access CourseAccess @default(OPEN)
33
51
  price Int @default(0)
34
- groupings String @default("[]")
35
52
  }
36
53
 
37
54
  model User {
38
- id Int @id @default(autoincrement())
39
- email String @unique
40
- password String
41
- name String?
42
- role String @default("student")
43
- resource String?
44
- createdAt DateTime @default(now())
45
- subscriptions Subscription[]
46
- communications Communication[]
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[]
47
64
  }
48
65
 
49
66
  model Admin {
@@ -58,19 +75,20 @@ model Admin {
58
75
  }
59
76
 
60
77
  model Library {
61
- slug String @id
78
+ slug String @id
62
79
  title String
63
80
  description String?
64
81
  thumbnail String?
65
82
  type ContentType
83
+ status LibraryStatus @default(DRAFT)
66
84
  body String?
67
85
  courseSlug String
68
86
  groupSlug String
69
- sortOrder Int @default(0)
70
- allowCommunication Boolean @default(true)
87
+ sortOrder Int @default(0)
88
+ allowCommunication Boolean @default(true)
71
89
  meta String?
72
- createdAt DateTime @default(now())
73
- updatedAt DateTime @updatedAt
90
+ createdAt DateTime @default(now())
91
+ updatedAt DateTime @updatedAt
74
92
  }
75
93
 
76
94
  model Subscription {
@@ -99,50 +117,46 @@ model Communication {
99
117
  readAt DateTime?
100
118
  createdAt DateTime @default(now())
101
119
  updatedAt DateTime @updatedAt
102
- user User @relation(fields: [userId], references: [id])
120
+ user User @relation(fields: [userId], references: [id])
103
121
 
104
122
  @@index([userId])
105
123
  @@index([librarySlug])
106
124
  }
107
- model Svg {
108
125
 
109
- slug String @id
126
+ model Svg {
127
+ slug String @id
110
128
 
111
- title String
129
+ title String
112
130
 
113
- body String
131
+ body String
114
132
 
115
- tags String @default("[]")
133
+ tags String @default("[]")
116
134
 
117
135
  createdAt DateTime @default(now())
118
136
 
119
137
  updatedAt DateTime @updatedAt
120
-
121
138
  }
122
139
 
123
140
  model Image {
141
+ slug String @id
124
142
 
125
- slug String @id
126
-
127
- title String
143
+ title String
128
144
 
129
- tags String @default("[]")
145
+ tags String @default("[]")
130
146
 
131
147
  createdAt DateTime @default(now())
132
148
 
133
149
  updatedAt DateTime @updatedAt
134
-
135
150
  }
136
- model Audio {
137
151
 
138
- slug String @id
152
+ model Audio {
153
+ slug String @id
139
154
 
140
- title String
155
+ title String
141
156
 
142
- tags String @default("[]")
157
+ tags String @default("[]")
143
158
 
144
159
  createdAt DateTime @default(now())
145
160
 
146
161
  updatedAt DateTime @updatedAt
147
-
148
- }
162
+ }
package/readme.md CHANGED
@@ -1,97 +1,505 @@
1
- ## Taleem Kernel API
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
- `taleem-kernel` is the shared DB/domain layer for Taleem applications. It provides Prisma/SQLite access and Taleem-specific modules.
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
- ### Core
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 // Prisma client
13
- kernel.config // configuration
14
- kernel.auth // authentication/JWT
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
- ### Modules
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
- Each module provides simple DB/domain operations such as:
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
- get(...)
38
- create(data)
39
- update(id, data)
40
- delete(id)
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
- with additional module-specific methods such as:
91
+ Registration creates a User and hashes the supplied password:
44
92
 
45
93
  ```js
46
- kernel.user.login()
47
- kernel.user.register()
48
- kernel.admin.isAdmin()
49
- kernel.library.listByCourse()
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
- ### Schema
100
+ Login returns a User JWT:
55
101
 
56
- The kernel owns the **canonical Taleem Prisma/SQLite schema**. Applications using Taleem DB should use the kernel rather than maintaining their own DB layer.
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
- kernel.svg.get(slug)
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
- `kernel.shutdown()` disconnects Prisma when the application exits.
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 two CLI commands for maintaining schema compatibility:
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 `prisma/schema.prisma` with the canonical schema shipped with `taleem-kernel`.
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 `prisma/schema.prisma`.
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
- ```text
90
- "@prisma/client": "^6.19.3",
91
- "bcrypt": "^6.0.0",
92
- "dotenv": "^17.4.2",
93
- "jsonwebtoken": "^9.0.3",
94
- "zod": "^4.4.3"
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
- The server should **not maintain its own Taleem Prisma models or DB modules**.
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
+
@@ -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);
@@ -1,9 +1,13 @@
1
- // src/serverKernel/modules/Admin.js
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) { this.kernel = 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
- if (!admin) throw new Error(`Admin.login(): Admin '${email}' not found.`);
21
- if (!admin.isActive) throw new Error(`Admin.login(): Admin '${email}' is inactive.`);
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
- return this.kernel.auth.createAdminToken(admin);
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
- if (createData.password) createData.password = await bcrypt.hash(createData.password, 10);
32
- return this.kernel.db.admin.create({ data: createData });
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
- if (updateData.password) updateData.password = await bcrypt.hash(updateData.password, 10);
38
- return this.kernel.db.admin.update({ where: { email }, data: updateData });
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({ where: { email } });
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
  }
@@ -6,21 +6,49 @@ export default class Course {
6
6
  if (filters.access) where.access = filters.access;
7
7
  return this.kernel.db.course.findMany({ where });
8
8
  }
9
-
10
9
  async get(slug) {
11
10
  return this.kernel.db.course.findUnique({ where: { slug } });
12
11
  }
13
-
14
- async create(data) {
15
- return this.kernel.db.course.create({ data });
12
+ async getGroups(courseSlug) {
13
+ return this.kernel.db.group.findMany({
14
+ where: { courseSlug },
15
+ orderBy: { id: 'asc' }
16
+ });
16
17
  }
17
-
18
- async update(slug, data) {
19
- return this.kernel.db.course.update({ where: { slug }, data });
18
+ async getGroup(courseSlug, groupSlug) {
19
+ return this.kernel.db.group.findUnique({
20
+ where: {
21
+ courseSlug_slug: { courseSlug, slug: groupSlug }
22
+ }
23
+ });
20
24
  }
21
-
22
- async delete(slug) {
23
- return this.kernel.db.course.delete({ where: { slug } });
25
+ async getGroupItems(courseSlug, groupSlug) {
26
+ const group = await this.getGroup(courseSlug, groupSlug);
27
+ return group?.items ?? [];
24
28
  }
29
+ async wipe() {
30
+ await this.kernel.db.group.deleteMany();
31
+ await this.kernel.db.course.deleteMany();
32
+ }
33
+ async seed(course) {
34
+ const { groupings = [], ...courseData } = course;
35
+
36
+ const created = await this.kernel.db.course.create({
37
+ data: courseData
38
+ });
25
39
 
40
+ for (const group of groupings) {
41
+ await this.kernel.db.group.create({
42
+ data: {
43
+ courseSlug: created.slug,
44
+ slug: group.slug,
45
+ title: group.title,
46
+ thumbnail: group.thumbnail ?? null,
47
+ items: group.items ?? []
48
+ }
49
+ });
50
+ }
51
+
52
+ return created;
53
+ }
26
54
  }
@@ -56,4 +56,13 @@ export default class Library {
56
56
  where: { slug }
57
57
  });
58
58
  }
59
+ async createFromSlot(slug, courseSlug, groupSlug, type) {
60
+ return this.create({
61
+ slug,
62
+ courseSlug,
63
+ groupSlug,
64
+ type,
65
+ title: slug
66
+ });
67
+ }
59
68
  }
@@ -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
- return this.kernel.db.user.create({ data: { ...data, password } });
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
- if (!user) throw new Error(`User.login(): User '${email}' not found.`);
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
- return this.kernel.auth.createUserToken(user);
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
- if (updateData.password) updateData.password = await bcrypt.hash(updateData.password, 10);
52
- return this.kernel.db.user.update({ where: { id }, data: updateData });
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({ where: { id } });
97
+ return this.kernel.db.user.delete({
98
+ where: { id }
99
+ });
57
100
  }
58
101
  }
@@ -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
- }