chore: init
This commit is contained in:
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": true,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
||||||
1
AGENTS.md
Normal file
1
AGENTS.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Do not use "Fake Axios". Only production APIs, even in tests.
|
||||||
12
README.md
Normal file
12
README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Atruna
|
||||||
|
|
||||||
|
Šis ir **NEOFICIĀLS** E-Klases API wrapperis.
|
||||||
|
|
||||||
|
Šis projekts **nav jebkādā veidā saistīts ar** SIA "Izglītības sistēmas" vai
|
||||||
|
E-Klasi un tās operatoriem. Visi zīmoli un pakalpojumu nosaukumi pieder to
|
||||||
|
attiecīgajiem īpašniekiem.
|
||||||
|
|
||||||
|
PROGRAMMATŪRA TIEK NODROŠINĀTA **"KĀ IR"**, BEZ JEBKĀDĀM GARANTIJĀM. ES
|
||||||
|
NEUZŅEMOS ATBILDĪBU JA TAVU KONTU NOBLOĶĒ.
|
||||||
|
|
||||||
|
Tu pats esi atbildīgs par to, kā šo lieto!
|
||||||
78
bun.lock
Normal file
78
bun.lock
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "eklase-api-wrapper",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"playwright-core": "^1.58.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||||
|
|
||||||
|
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||||
|
|
||||||
|
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||||
|
|
||||||
|
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||||
|
|
||||||
|
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"playwright-core": ["playwright-core@1.58.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw=="],
|
||||||
|
|
||||||
|
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
29
jsconfig.json
Normal file
29
jsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "eklase-api-wrapper",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"playwright-core": "^1.58.0"
|
||||||
|
},
|
||||||
|
"description": "TypeScript Bun wrapper for the e-klase authentication API with tests.",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun build",
|
||||||
|
"test": "bun test"
|
||||||
|
},
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Client/index.ts
Normal file
23
src/Client/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { EklaseAuthClient } from "../util/AuthClient";
|
||||||
|
import { Evaluation, EvaluationDetails, LessonTime, UserInfo } from "./types";
|
||||||
|
|
||||||
|
export class APIClient extends EklaseAuthClient {
|
||||||
|
|
||||||
|
async getUser(): Promise<UserInfo> {
|
||||||
|
return await this.get<UserInfo>("https://family.e-klase.lv/api/user")
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvaluations(): Promise<Evaluation[]> {
|
||||||
|
return await this.get<Evaluation[]>("https://family.e-klase.lv/api/evaluations")
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvaluationsDetails(evaluationId: number): Promise<EvaluationDetails> {
|
||||||
|
return await this.get<EvaluationDetails>(`https://family.e-klase.lv/api/evaluations/${evaluationId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getLessonTimes(): Promise<LessonTime[]> {
|
||||||
|
return await this.get<LessonTime[]>("https://family.e-klase.lv/api/lesson-times")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
150
src/Client/types.ts
Normal file
150
src/Client/types.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
export type UserInfo = {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
personType: "Student";
|
||||||
|
personTypeName: "skolēns" | string;
|
||||||
|
personTypeId: number;
|
||||||
|
school: {
|
||||||
|
id: number;
|
||||||
|
tenantId: string;
|
||||||
|
name: string;
|
||||||
|
regionId: number;
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
},
|
||||||
|
studyYearId: number;
|
||||||
|
activeStudyYears: {
|
||||||
|
id: number;
|
||||||
|
name: `${number}./${number}.` | string;
|
||||||
|
isCurrent: boolean
|
||||||
|
}[],
|
||||||
|
userPublicId: string;
|
||||||
|
username: string;
|
||||||
|
/** phone # without +371 */
|
||||||
|
phoneNumber: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
userSettings: Record<string, unknown>;
|
||||||
|
/** ģimenes komplekts */
|
||||||
|
premiumSubscription?: {
|
||||||
|
isActive?: boolean,
|
||||||
|
endDate?: string,
|
||||||
|
hasDiscount?: boolean,
|
||||||
|
isActiveTillEndOfYear?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Evaluation = {
|
||||||
|
id: number,
|
||||||
|
/** bad grade: 1-3; (1-4 if professional) */
|
||||||
|
value: string,
|
||||||
|
/** iso 8601 */
|
||||||
|
timeCreated: string,
|
||||||
|
/** class name e.g. math */
|
||||||
|
disciplineName: string,
|
||||||
|
isNonAttendance: boolean
|
||||||
|
lesson?: {
|
||||||
|
/** iso 8601 */
|
||||||
|
date: string,
|
||||||
|
type?: {
|
||||||
|
name: string,
|
||||||
|
abbreviation: string,
|
||||||
|
/** ltid_Test = PD */
|
||||||
|
lessonTypeId: string
|
||||||
|
},
|
||||||
|
isTest: boolean,
|
||||||
|
/** html-like rich text markup */
|
||||||
|
subject?: boolean
|
||||||
|
},
|
||||||
|
isWeighted: boolean,
|
||||||
|
isNew: boolean,
|
||||||
|
hasComments: boolean,
|
||||||
|
editedEvaluation?: {
|
||||||
|
/** bad grade: 1-3; (1-4 if professional) */
|
||||||
|
value: string,
|
||||||
|
isNonAttendance: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LessonTime = {
|
||||||
|
name: string,
|
||||||
|
from: `${number}:${number}`
|
||||||
|
to: `${number}:${number}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EvaluationDetails = {
|
||||||
|
evaluation: EvaluationDetailsDetailed;
|
||||||
|
lesson: EvaluationLessonDetails;
|
||||||
|
editedEvaluations: EditedEvaluation[];
|
||||||
|
statistics: EvaluationStatistics;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EvaluationDetailsDetailed = {
|
||||||
|
id: number;
|
||||||
|
/** bad grade: 1-3; (1-4 if professional) */
|
||||||
|
value: string;
|
||||||
|
/** Vārds Uzvārds */
|
||||||
|
authorName: string;
|
||||||
|
/** iso 8601 */
|
||||||
|
timeCreated: string;
|
||||||
|
testNotes: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EvaluationLessonDetails = {
|
||||||
|
type: LessonType;
|
||||||
|
/** iso 8601 midnight */
|
||||||
|
lessonDate: string;
|
||||||
|
lessonSubject: LessonSubject;
|
||||||
|
disciplineName: string;
|
||||||
|
testId: number;
|
||||||
|
lessonNumberInDiary: number;
|
||||||
|
creation: LessonAudit;
|
||||||
|
lastModification: LessonModification;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LessonType = {
|
||||||
|
name: string;
|
||||||
|
abbreviation: string;
|
||||||
|
/** e.g. ltid_Test */
|
||||||
|
lessonTypeId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LessonSubject = {
|
||||||
|
/** html / rich text */
|
||||||
|
text: string;
|
||||||
|
attachments: LessonAttachment[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LessonAttachment = Record<string, unknown>;
|
||||||
|
|
||||||
|
|
||||||
|
export type LessonAudit = {
|
||||||
|
authorName: string;
|
||||||
|
/** iso 8601 */
|
||||||
|
timeCreated: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LessonModification = {
|
||||||
|
authorName: string;
|
||||||
|
/** iso 8601 */
|
||||||
|
timeModified: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditedEvaluation = {
|
||||||
|
value: string;
|
||||||
|
authorName: string;
|
||||||
|
/** iso 8601 */
|
||||||
|
timeCreated: string;
|
||||||
|
/** e.g. Student_Improved */
|
||||||
|
reasonForEdit: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EvaluationStatistics = {
|
||||||
|
chartItems: number[];
|
||||||
|
/** 1st, 2nd, 3rd and so on */
|
||||||
|
studentPlace: number;
|
||||||
|
indexOfStudentEvaluation: number;
|
||||||
|
averageClassEvaluation: string;
|
||||||
|
};
|
||||||
2
src/index.ts
Normal file
2
src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./Client";
|
||||||
|
export * from "./util/NameUtil";
|
||||||
43
src/login/index.ts
Normal file
43
src/login/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { sleep } from "bun";
|
||||||
|
import { getBrowserContext } from "./playwright";
|
||||||
|
|
||||||
|
export const LOGIN_URL = "https://family.e-klase.lv/";
|
||||||
|
|
||||||
|
export async function loginAndGetToken({
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<{
|
||||||
|
expiry: Date;
|
||||||
|
token: string;
|
||||||
|
}> {
|
||||||
|
const ctx = await getBrowserContext();
|
||||||
|
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||||
|
await page.goto(LOGIN_URL);
|
||||||
|
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
await page.locator("#username").fill(username, { force: true });
|
||||||
|
await page.locator("#password").fill(password, { force: true });
|
||||||
|
await page.locator("#login-button").click();
|
||||||
|
|
||||||
|
const req = await page.waitForRequest(
|
||||||
|
(request) =>
|
||||||
|
request.method() === "GET" &&
|
||||||
|
request.url() === "https://family.e-klase.lv/api/user"
|
||||||
|
);
|
||||||
|
|
||||||
|
const authHeader = req.headers()["authorization"];
|
||||||
|
|
||||||
|
await ctx.close();
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: authHeader,
|
||||||
|
expiry: new Date(Date.now() + 15 * 60 * 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
22
src/login/playwright.ts
Normal file
22
src/login/playwright.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Browser, BrowserContext, chromium } from "playwright-core";
|
||||||
|
|
||||||
|
const userAgents = [
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0"
|
||||||
|
];
|
||||||
|
|
||||||
|
let _globalBrowserCtx: Browser;
|
||||||
|
|
||||||
|
export async function getBrowserContext(): Promise<BrowserContext> {
|
||||||
|
if (!_globalBrowserCtx) {
|
||||||
|
_globalBrowserCtx = await chromium.launch({
|
||||||
|
executablePath:
|
||||||
|
Bun.which("google-chrome") ||
|
||||||
|
Bun.which("chrome") ||
|
||||||
|
Bun.which("google-chrome-stable")!,
|
||||||
|
headless: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await _globalBrowserCtx.newContext({
|
||||||
|
userAgent: userAgents[Math.floor(Math.random() * userAgents.length)]
|
||||||
|
});
|
||||||
|
}
|
||||||
67
src/util/AuthClient.ts
Normal file
67
src/util/AuthClient.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { loginAndGetToken } from "../login";
|
||||||
|
|
||||||
|
export type AuthClientOptions = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TokenResponse = {
|
||||||
|
expiry: Date;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class EklaseAuthClient {
|
||||||
|
#username: string;
|
||||||
|
#password: string;
|
||||||
|
#token?: TokenResponse;
|
||||||
|
|
||||||
|
constructor(options: AuthClientOptions) {
|
||||||
|
this.#username = options.username;
|
||||||
|
this.#password = options.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getToken(): Promise<TokenResponse> {
|
||||||
|
if (!this.#token || new Date() > this.#token.expiry) {
|
||||||
|
const r = await loginAndGetToken({
|
||||||
|
username: this.#username,
|
||||||
|
password: this.#password
|
||||||
|
});
|
||||||
|
this.#token = r;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
return this.#token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async apiRequest(
|
||||||
|
input: RequestInfo,
|
||||||
|
init: RequestInit = {}
|
||||||
|
): Promise<Response> {
|
||||||
|
const tokenResponse = await this.getToken();
|
||||||
|
const headers = new Headers(init.headers ?? {});
|
||||||
|
if (!headers.has("authorization")) {
|
||||||
|
headers.set("authorization", tokenResponse.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(
|
||||||
|
input: RequestInfo,
|
||||||
|
init: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const tokenResponse = await this.getToken();
|
||||||
|
const headers = new Headers(init.headers ?? {});
|
||||||
|
if (!headers.has("authorization")) {
|
||||||
|
headers.set("authorization", tokenResponse.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await fetch(input, {
|
||||||
|
...init,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
return await r.json() as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/util/Mutex.ts
Normal file
27
src/util/Mutex.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export class Mutex {
|
||||||
|
private promise: Promise<void> = Promise.resolve();
|
||||||
|
private resolve: () => void = () => {};
|
||||||
|
|
||||||
|
async withMutex<T>(f: () => Promise<T>): Promise<T> {
|
||||||
|
this.lock();
|
||||||
|
let r;
|
||||||
|
try {
|
||||||
|
r = await f();
|
||||||
|
} catch (e_) {
|
||||||
|
this.unlock();
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
this.unlock();
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
async lock(): Promise<void> {
|
||||||
|
const oldPromise = this.promise;
|
||||||
|
this.promise = new Promise((resolve) => (this.resolve = resolve));
|
||||||
|
await oldPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock(): void {
|
||||||
|
this.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/util/NameUtil.ts
Normal file
10
src/util/NameUtil.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Normalizes a name from the Latvian Government form (LastName FirstName) or (LastName-OtherLastName FirstName)
|
||||||
|
*/
|
||||||
|
export function normalizeName(name: string): {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
} {
|
||||||
|
const [lastName, firstName] = name.split(" ");
|
||||||
|
return { lastName, firstName };
|
||||||
|
}
|
||||||
16
tests/EklaseAuthClient.test.ts
Normal file
16
tests/EklaseAuthClient.test.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { APIClient } from "../src/Client";
|
||||||
|
|
||||||
|
test.todo("Authenticated API request", async () => {
|
||||||
|
const client = new APIClient({
|
||||||
|
username: process.env.USERNAME!,
|
||||||
|
password: process.env.PASSWORD!
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await client.getToken();
|
||||||
|
expect(typeof token.token).toBe("string");
|
||||||
|
console.log(token);
|
||||||
|
|
||||||
|
const user = await client.apiRequest("https://family.e-klase.lv/api/user");
|
||||||
|
console.log(await user.json());
|
||||||
|
});
|
||||||
21
tests/NameUtil.test.ts
Normal file
21
tests/NameUtil.test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { normalizeName } from "../src";
|
||||||
|
|
||||||
|
// LV/EN placeholder names
|
||||||
|
test.each([
|
||||||
|
// https://en.wikipedia.org/wiki/List_of_placeholder_names#Latvian
|
||||||
|
{
|
||||||
|
fn: "Andris",
|
||||||
|
ln: "Paraudziņš"
|
||||||
|
},
|
||||||
|
// https://en.wikipedia.org/wiki/List_of_placeholder_names#English
|
||||||
|
{
|
||||||
|
fn: "John",
|
||||||
|
ln: "Doe"
|
||||||
|
}
|
||||||
|
])("Full government name normalization info FN/LN", (v) => {
|
||||||
|
// why last name first?
|
||||||
|
const x = normalizeName(`${v.ln} ${v.fn}`);
|
||||||
|
expect(x.firstName).toBe(v.fn);
|
||||||
|
expect(x.lastName).toBe(v.ln);
|
||||||
|
});
|
||||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"allowJs": false,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["src", "tests"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user