├── .npmrc ├── apps ├── web-mars │ ├── public │ │ ├── robots.txt │ │ ├── cover.png │ │ ├── favicon.ico │ │ ├── icon-192.png │ │ ├── icon-512.png │ │ ├── apple-touch-icon.png │ │ ├── _headers │ │ └── icon.svg │ ├── .env.development │ ├── src │ │ ├── services │ │ │ ├── bus.js │ │ │ └── notify.js │ │ ├── components │ │ │ ├── base │ │ │ │ ├── Icon.vue │ │ │ │ ├── tabs │ │ │ │ │ ├── Tabs.vue │ │ │ │ │ └── Tab.vue │ │ │ │ ├── Checkbox.vue │ │ │ │ ├── loaders │ │ │ │ │ ├── LoadingDots.vue │ │ │ │ │ └── LoadingOverlay.vue │ │ │ │ ├── Image.vue │ │ │ │ ├── notifications │ │ │ │ │ ├── Notifications.vue │ │ │ │ │ └── Notification.vue │ │ │ │ ├── Switch.vue │ │ │ │ ├── Select.vue │ │ │ │ ├── Modal.vue │ │ │ │ ├── Input.vue │ │ │ │ ├── Button.vue │ │ │ │ └── app │ │ │ │ │ └── Logo.vue │ │ │ └── layout │ │ │ │ ├── ThemeToggler.vue │ │ │ │ ├── modal-containers │ │ │ │ ├── SubjectContainer.vue │ │ │ │ ├── AvailabilityContainer.vue │ │ │ │ └── settings │ │ │ │ │ ├── SettingsContainerSlider.vue │ │ │ │ │ └── SettingsContainer.vue │ │ │ │ ├── ScrollPicker.vue │ │ │ │ └── subject │ │ │ │ ├── SubjectDiarySection.vue │ │ │ │ ├── SubjectDiary.vue │ │ │ │ └── SubjectGrades.vue │ │ ├── main.js │ │ ├── stores │ │ │ ├── health.js │ │ │ ├── loader.js │ │ │ ├── subject.js │ │ │ ├── grades.js │ │ │ ├── settings.js │ │ │ ├── auth.js │ │ │ ├── years.js │ │ │ ├── terms.js │ │ │ └── diary.js │ │ ├── assets │ │ │ └── globals.css │ │ ├── App.vue │ │ ├── utils │ │ │ └── index.js │ │ ├── config │ │ │ └── index.js │ │ ├── api │ │ │ └── index.js │ │ └── views │ │ │ ├── Login.vue │ │ │ └── Dashboard.vue │ ├── .eslintrc.cjs │ ├── windi.config.js │ ├── vite.config.js │ ├── package.json │ └── index.html └── api │ ├── src │ ├── routes │ │ ├── health │ │ │ ├── index.js │ │ │ └── sms │ │ │ │ └── index.js │ │ ├── city │ │ │ └── index.js │ │ ├── dashboard │ │ │ ├── years │ │ │ │ └── index.js │ │ │ ├── terms │ │ │ │ └── _yearID │ │ │ │ │ └── index.js │ │ │ ├── subject │ │ │ │ ├── rubrics │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── grades │ │ │ │ └── index.js │ │ │ └── diary │ │ │ │ └── _termID │ │ │ │ └── index.js │ │ └── login │ │ │ ├── captchaRefresh │ │ │ └── index.js │ │ │ └── index.js │ ├── plugins │ │ ├── handlers │ │ │ ├── notFoundHandler.js │ │ │ └── errorHandler.js │ │ ├── cookieParse.js │ │ ├── hooks │ │ │ └── preValidation.js │ │ ├── mergeCookies.js │ │ └── api.js │ ├── schema │ │ ├── domain.js │ │ ├── token.js │ │ └── city.js │ ├── config │ │ └── index.js │ ├── utils │ │ └── crypto.js │ └── index.js │ ├── .eslintrc.cjs │ └── package.json ├── .vscode ├── settings.json └── extensions.json ├── .github ├── media │ └── swagger.png └── workflows │ ├── ci.yml │ ├── cd.yml │ └── deploy-dev.yml ├── packages └── shared │ ├── src │ ├── index.ts │ ├── current-quarter.ts │ └── config.ts │ ├── vite.config.ts │ ├── tsconfig.json │ └── package.json ├── turbo.json ├── .env.development ├── .gitignore ├── package.json ├── LICENSE └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true -------------------------------------------------------------------------------- /apps/web-mars/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.packageManager": "pnpm" 3 | } 4 | -------------------------------------------------------------------------------- /apps/web-mars/.env.development: -------------------------------------------------------------------------------- 1 | VITE_SERVER_URL="http://localhost:4000" -------------------------------------------------------------------------------- /.github/media/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyrange/enis2/HEAD/.github/media/swagger.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/web-mars/public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyrange/enis2/HEAD/apps/web-mars/public/cover.png -------------------------------------------------------------------------------- /apps/web-mars/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyrange/enis2/HEAD/apps/web-mars/public/favicon.ico -------------------------------------------------------------------------------- /apps/web-mars/public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyrange/enis2/HEAD/apps/web-mars/public/icon-192.png -------------------------------------------------------------------------------- /apps/web-mars/public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyrange/enis2/HEAD/apps/web-mars/public/icon-512.png -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./config" 2 | export { getCurrentQuarter } from "./current-quarter" 3 | -------------------------------------------------------------------------------- /apps/web-mars/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anyrange/enis2/HEAD/apps/web-mars/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/web-mars/src/services/bus.js: -------------------------------------------------------------------------------- 1 | import { createNanoEvents } from "nanoevents" 2 | 3 | const emitter = createNanoEvents() 4 | 5 | export { emitter } 6 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/Icon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /apps/api/src/routes/health/index.js: -------------------------------------------------------------------------------- 1 | export default async function (fastify) { 2 | fastify.get("", { schema: { tags: ["miscellaneous"] } }, () => { 3 | return { message: "I'm alive" } 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /apps/api/src/plugins/handlers/notFoundHandler.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | 3 | export default fp(async function plugin(fastify) { 4 | fastify.setNotFoundHandler((req, reply) => { 5 | reply.code(404).send({ message: "Service not found" }) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /apps/api/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: "eslint:recommended", 7 | overrides: [], 8 | parserOptions: { 9 | ecmaVersion: "latest", 10 | sourceType: "module", 11 | }, 12 | rules: {}, 13 | } 14 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "start": {}, 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "dev": { 10 | "cache": false, 11 | "persistent": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/api/src/schema/domain.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | 3 | export default fp(async function plugin(fastify) { 4 | fastify.addSchema({ 5 | $id: "domain", 6 | title: "City in query", 7 | type: "object", 8 | properties: { 9 | city: fastify.getSchema("city"), 10 | }, 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /apps/web-mars/public/_headers: -------------------------------------------------------------------------------- 1 | / 2 | Cache-Control: public, max-age=0, s-maxage=0, must-revalidate 3 | 4 | /assets/* 5 | Cache-Control: public, max-age=31536000, immutable 6 | 7 | /workbox-* 8 | Cache-Control: public, max-age=31536000, immutable 9 | 10 | /manifest.webmanifest 11 | Content-Type: application/manifest+json 12 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # sometimes SMS API's doesn't have valid SSL certificates 2 | NODE_TLS_REJECT_UNAUTHORIZED="0" 3 | 4 | # API: 5 | PORT=4000 6 | JWT_SECRET="TheArtOfDying" 7 | CRYPT_KEY="huEaof9iV1XOWd1oMrrp9zxc20D1VM6k" 8 | IPINFO_TOKEN="xXxXxXxXxXxXxX" # not required 9 | 10 | # WEB: 11 | VITE_SERVER_URL="http://localhost:${PORT}" 12 | -------------------------------------------------------------------------------- /apps/api/src/schema/token.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | 3 | export default fp(async function plugin(fastify) { 4 | fastify.addSchema({ 5 | $id: "token", 6 | title: "Require token in header", 7 | type: "object", 8 | required: ["authorization"], 9 | properties: { authorization: { type: "string" } }, 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /apps/api/src/schema/city.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | import { SCHOOLS } from "@enis2/shared" 3 | 4 | const cityValues = SCHOOLS.map((school) => school.value) 5 | 6 | export default fp(async function plugin(fastify) { 7 | fastify.addSchema({ 8 | $id: "city", 9 | title: "City", 10 | type: "string", 11 | enum: cityValues, 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /apps/api/src/plugins/cookieParse.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | 3 | const plugin = fp(async function plugin(fastify) { 4 | fastify.decorate("cookieParse", (res) => { 5 | const rawCookies = res.headers.raw()["set-cookie"] 6 | 7 | if (!rawCookies) return null 8 | 9 | return rawCookies.map((cookie) => cookie.split(";")[0]).join("; ") 10 | }) 11 | }) 12 | 13 | export default plugin 14 | -------------------------------------------------------------------------------- /packages/shared/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import { defineConfig } from "vite" 3 | import dts from "vite-plugin-dts" 4 | 5 | // https://vitejs.dev/guide/build.html#library-mode 6 | export default defineConfig({ 7 | build: { 8 | lib: { 9 | entry: resolve(__dirname, "src/index.ts"), 10 | name: "shared", 11 | fileName: "shared", 12 | }, 13 | }, 14 | plugins: [dts()], 15 | }) 16 | -------------------------------------------------------------------------------- /apps/web-mars/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution") 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-prettier", 10 | ], 11 | rules: { 12 | "vue/multi-word-component-names": "off", 13 | }, 14 | parserOptions: { 15 | ecmaVersion: "latest", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /apps/web-mars/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue" 2 | import { createPinia } from "pinia" 3 | import { registerSW } from "virtual:pwa-register" 4 | import VWave from "v-wave" 5 | import App from "./App.vue" 6 | import "virtual:windi.css" 7 | import "./assets/globals.css" 8 | 9 | registerSW({ immediate: true }) 10 | 11 | const app = createApp(App) 12 | const pinia = createPinia() 13 | 14 | app.use(VWave).use(pinia).mount("#app") 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules 4 | dist 5 | build 6 | web-build 7 | 8 | .turbo 9 | *.log 10 | *.local 11 | .cache 12 | 13 | # local env files 14 | .env.local 15 | .env.*.local 16 | .env 17 | 18 | # Log files 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | pnpm-debug.log* 23 | 24 | # Editor directories and files 25 | .idea 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw? 31 | # Local Netlify folder 32 | .netlify 33 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/tabs/Tabs.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/health.js: -------------------------------------------------------------------------------- 1 | import { ref } from "vue" 2 | import { defineStore } from "pinia" 3 | import { checkHealth } from "../api" 4 | 5 | export default defineStore("health", () => { 6 | const showAvailabilityModal = ref(false) 7 | 8 | const checkAvailability = async () => { 9 | try { 10 | const { alive } = await checkHealth() 11 | !alive && (showAvailabilityModal.value = true) 12 | 13 | return Promise.resolve(alive) 14 | } catch (error) { 15 | return Promise.reject(error) 16 | } 17 | } 18 | 19 | return { 20 | showAvailabilityModal, 21 | checkAvailability, 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /apps/api/src/config/index.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv" 2 | 3 | dotenv.config() 4 | 5 | const { IPINFO_TOKEN, PORT = 4000, JWT_SECRET, CRYPT_KEY } = process.env 6 | 7 | if (!JWT_SECRET) { 8 | throw new Error("JWT_SECRET is missing") 9 | } 10 | 11 | if (!CRYPT_KEY) { 12 | throw new Error("CRYPT_KEY is missing") 13 | } 14 | 15 | if (!IPINFO_TOKEN) { 16 | console.warn("IPINFO_TOKEN is missing") 17 | } 18 | 19 | const FAKE_USER_AGENT = 20 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" 21 | 22 | export { PORT, JWT_SECRET, CRYPT_KEY, FAKE_USER_AGENT, IPINFO_TOKEN } 23 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/api/src/plugins/hooks/preValidation.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | 3 | const plugin = fp(async function plugin(fastify) { 4 | fastify.decorateRequest("cookies", "") 5 | fastify.decorateRequest("account", "") 6 | 7 | fastify.addHook("preValidation", async (req) => { 8 | if (!req.headers.authorization) return 9 | try { 10 | const token = req.headers.authorization.replace("Bearer ", "") 11 | fastify.jwt.verify(token, (err, decoded) => { 12 | if (err) throw err 13 | req.cookies = decoded.cookies 14 | req.account = decoded.account 15 | }) 16 | } catch (e) { 17 | return 18 | } 19 | }) 20 | }) 21 | 22 | export default plugin 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - production 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | name: "Build and lint" 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v2 18 | 19 | - name: Set node version to 18 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18 23 | cache: "pnpm" 24 | 25 | - run: pnpm install 26 | 27 | - name: Build 28 | run: pnpm run build 29 | 30 | - name: Lint 31 | run: pnpm run lint 32 | 33 | - name: Check formatting 34 | run: pnpm run prettier 35 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@enis2/shared", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "main": "./dist/shared.umd.cjs", 7 | "module": "./dist/shared.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/shared.js", 12 | "require": "./dist/shared.umd.cjs" 13 | } 14 | }, 15 | "scripts": { 16 | "build": "tsc && vite build" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "prettier": { 22 | "semi": false 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^18.16.8", 26 | "prettier": "^2.8.8", 27 | "typescript": "^5.0.2", 28 | "vite": "^4.3.2", 29 | "vite-plugin-dts": "^2.3.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/web-mars/src/services/notify.js: -------------------------------------------------------------------------------- 1 | import { nanoid } from "nanoid" 2 | import { emitter } from "./bus" 3 | 4 | /** 5 | * @param {('info'|'success'|'warning'|'danger')} type 6 | */ 7 | 8 | const notify = { 9 | show: ({ 10 | id = nanoid(), 11 | message, 12 | type = "info", 13 | delay = 3000, 14 | progress = true, 15 | closable = true, 16 | actions = {}, 17 | }) => { 18 | emitter.emit("newNotification", { 19 | id, 20 | message, 21 | type, 22 | delay, 23 | progress, 24 | closable, 25 | actions, 26 | }) 27 | }, 28 | dismiss: (id) => { 29 | emitter.emit("dismissNotification", id) 30 | }, 31 | clear: () => { 32 | emitter.emit("clearNotifications") 33 | }, 34 | } 35 | 36 | export { notify } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enis2", 3 | "private": true, 4 | "description": "convenient, fast, adaptive client for e-journal used in NIS", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "dotenv -- turbo run start", 8 | "build": "rimraf web-build && stale-dep && dotenv -- turbo run build", 9 | "dev": "stale-dep && dotenv -- turbo run dev", 10 | "postinstall": "npx only-allow yarn && stale-dep -u && yarn build --filter @enis2/shared" 11 | }, 12 | "workspaces": [ 13 | "apps/*", 14 | "packages/*" 15 | ], 16 | "engines": { 17 | "node": ">=16.14.0" 18 | }, 19 | "devDependencies": { 20 | "cross-env": "^7.0.3", 21 | "dotenv-cli": "^7.2.1", 22 | "rimraf": "^5.0.1", 23 | "stale-dep": "^0.6.0", 24 | "turbo": "^1.10.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - production 7 | push: 8 | branches: 9 | - production 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Deploy server on VPS 16 | uses: appleboy/ssh-action@master 17 | with: 18 | host: ${{secrets.HOST}} 19 | username: ${{ secrets.USERNAME }} 20 | key: ${{ secrets.SSH_PRIVATE_KEY }} 21 | port: 22 22 | script: | 23 | export NVM_DIR=~/.nvm 24 | source ~/.nvm/nvm.sh 25 | cd apps/enis2 26 | git fetch 27 | git reset --hard origin/main 28 | npx pnpm install --filter api --store=node_modules/.pnpm-store 29 | npx pm2 restart enis2 30 | -------------------------------------------------------------------------------- /apps/api/src/plugins/mergeCookies.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | 3 | const stringToObject = (cookieString) => { 4 | const arrayOfCookies = cookieString 5 | .split("; ") 6 | .filter((c) => !!c.length) // filter empty values 7 | .map((c) => c.split("=")) 8 | 9 | return Object.fromEntries(arrayOfCookies) 10 | } 11 | 12 | const objectToString = (cookieObject) => { 13 | return Object.entries(cookieObject) 14 | .map((c) => c.join("=")) 15 | .join("; ") 16 | } 17 | 18 | const plugin = fp(async function plugin(fastify) { 19 | fastify.decorate("mergeCookies", (oldCookie, newCookie) => { 20 | const mergedCookie = Object.assign( 21 | stringToObject(oldCookie), 22 | stringToObject(newCookie) 23 | ) 24 | 25 | return objectToString(mergedCookie) 26 | }) 27 | }) 28 | 29 | export default plugin 30 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/ThemeToggler.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /apps/web-mars/windi.config.js: -------------------------------------------------------------------------------- 1 | import colors from "windicss/colors" 2 | 3 | export default { 4 | darkMode: "class", 5 | shortcuts: { 6 | "default-focus": "outline-none focus:outline-none focus-visible:ring-2", 7 | }, 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: colors.blue[500], 12 | secondary: { 13 | darkest: "#121212", 14 | darker: "#1d1d1d", 15 | dark: "#282828", 16 | DEFAULT: "#333333", 17 | light: "#969696", 18 | lighter: "#ABABAB", 19 | lightest: "#BFBFBF", 20 | }, 21 | q: { 22 | positive: "#21ba45", 23 | negative: "#c20318", 24 | warning: "#f2c037", 25 | info: "#31ccec", 26 | }, 27 | }, 28 | fontSize: { 29 | xxs: "0.7rem", 30 | }, 31 | }, 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.0", 4 | "description": "simple API to bypass the CORS policy", 5 | "license": "MIT", 6 | "author": "thaseyor", 7 | "type": "module", 8 | "scripts": { 9 | "dev": "nodemon src/index.js", 10 | "start": "node src/index.js" 11 | }, 12 | "prettier": { 13 | "semi": false 14 | }, 15 | "dependencies": { 16 | "@enis2/shared": "*", 17 | "@fastify/autoload": "^5.4.1", 18 | "@fastify/compress": "6.1.0", 19 | "@fastify/cors": "^8.1.0", 20 | "@fastify/jwt": "^6.3.2", 21 | "@fastify/swagger": "7.4.1", 22 | "dotenv": "^16.0.3", 23 | "fastify": "^4.9.2", 24 | "fastify-plugin": "^4.3.0", 25 | "node-fetch": "^3.2.10" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^8.39.0", 29 | "nodemon": "^2.0.20", 30 | "prettier": "^2.8.8" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/api/src/utils/crypto.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto" 2 | import { CRYPT_KEY } from "../config/index.js" 3 | 4 | const algorithm = "aes-256-ctr" 5 | const iv = crypto.randomBytes(16) 6 | 7 | export const encrypt = (text) => { 8 | if (!text) return null 9 | const cipher = crypto.createCipheriv(algorithm, CRYPT_KEY, iv) 10 | 11 | const encrypted = Buffer.concat([cipher.update(text), cipher.final()]) 12 | 13 | return { 14 | iv: iv.toString("hex"), 15 | content: encrypted.toString("hex"), 16 | } 17 | } 18 | 19 | export const decrypt = (hash) => { 20 | if (!hash) return null 21 | 22 | const decipher = crypto.createDecipheriv( 23 | algorithm, 24 | CRYPT_KEY, 25 | Buffer.from(hash.iv, "hex") 26 | ) 27 | 28 | const decrpyted = Buffer.concat([ 29 | decipher.update(Buffer.from(hash.content, "hex")), 30 | decipher.final(), 31 | ]) 32 | 33 | return decrpyted.toString() 34 | } 35 | -------------------------------------------------------------------------------- /apps/api/src/routes/city/index.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch" 2 | import { IPINFO_TOKEN } from "../../config/index.js" 3 | 4 | export default async function (fastify) { 5 | fastify.get( 6 | "", 7 | { 8 | schema: { 9 | response: { 10 | 200: { 11 | type: "object", 12 | properties: { 13 | city: { type: "string" }, 14 | region: { type: "string" }, 15 | }, 16 | }, 17 | }, 18 | tags: ["miscellaneous"], 19 | }, 20 | }, 21 | async (req, reply) => { 22 | const token = IPINFO_TOKEN 23 | 24 | const requestIp = req.ips[req.ips.length - 1] 25 | 26 | const res = await fetch( 27 | `https://ipinfo.io/${requestIp}/json?token=${token}` 28 | ).then((res) => res.json()) 29 | 30 | await reply.send({ city: res.city || "", region: res.region || "" }) 31 | } 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /apps/api/src/routes/health/sms/index.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch" 2 | 3 | const SECOND = 1000 4 | 5 | export default async function (fastify) { 6 | fastify.get( 7 | "", 8 | { 9 | schema: { 10 | querystring: fastify.getSchema("domain"), 11 | tags: ["miscellaneous"], 12 | }, 13 | }, 14 | async (req, reply) => { 15 | const controller = new AbortController() 16 | 17 | const timeoutId = setTimeout(() => controller.abort(), 15 * SECOND) 18 | 19 | try { 20 | const res = await fetch( 21 | `https://sms.${req.query.city}.nis.edu.kz/root`, 22 | { 23 | redirect: "manual", 24 | signal: controller.signal, 25 | } 26 | ) 27 | 28 | clearTimeout(timeoutId) 29 | 30 | return reply.send({ alive: res.status < 400 }) 31 | } catch { 32 | return reply.send({ alive: false }) 33 | } 34 | } 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy-dev.yml: -------------------------------------------------------------------------------- 1 | name: Deploy dev server 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | REF: ${{ github.ref }} 8 | REF_NAME: ${{ github.ref_name }} 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Deploy dev server on VPS 15 | uses: appleboy/ssh-action@master 16 | with: 17 | envs: REF,REF_NAME 18 | host: ${{secrets.HOST}} 19 | username: ${{ secrets.USERNAME }} 20 | key: ${{ secrets.SSH_PRIVATE_KEY }} 21 | port: 22 22 | script: | 23 | export NVM_DIR=~/.nvm 24 | source ~/.nvm/nvm.sh 25 | cd apps/enis2-dev 26 | echo $REF 27 | echo $REF_NAME 28 | git fetch 29 | git checkout $REF_NAME 30 | git status 31 | npx pnpm install --filter api --store=node_modules/.pnpm-store 32 | npx pm2 restart enis2-dev 33 | -------------------------------------------------------------------------------- /apps/web-mars/src/assets/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | overflow: overlay; 9 | @apply text-black dark:text-white antialiased; 10 | @apply bg-gray-100 dark:bg-secondary-darkest; 11 | } 12 | 13 | :root { 14 | --autofill-background: #eff6ff; 15 | --autofill-color: #000000; 16 | --scroll-picker-border-color: white; 17 | --scroll-picker-item-color: #333; 18 | } 19 | 20 | html.dark, 21 | :root { 22 | --autofill-background: #282828; 23 | --autofill-color: #ffffff; 24 | --scroll-picker-border-color: #1d1d1d; 25 | --scroll-picker-item-color: #ababab; 26 | } 27 | 28 | html.dark { 29 | color-scheme: dark; 30 | } 31 | 32 | .fade-enter-active, 33 | .fade-leave-active { 34 | transition: opacity 0.5s; 35 | } 36 | .fade-enter, 37 | .fade-leave-to { 38 | opacity: 0; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 anyrange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/api/src/routes/dashboard/years/index.js: -------------------------------------------------------------------------------- 1 | export default async function (fastify) { 2 | fastify.get( 3 | "", 4 | { 5 | schema: { 6 | querystring: fastify.getSchema("domain"), 7 | headers: fastify.getSchema("token"), 8 | response: { 9 | 200: { 10 | type: "array", 11 | items: { 12 | type: "object", 13 | properties: { 14 | Name: { type: "string" }, 15 | Id: { type: "string" }, 16 | isActual: { type: "boolean" }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | tags: ["dashboard"], 22 | }, 23 | }, 24 | async (req, reply) => { 25 | const cookie = req.cookies 26 | 27 | const { data: years } = await fastify.api({ 28 | url: `https://sms.${req.query.city}.nis.edu.kz/Ref/GetSchoolYears?fullData=true`, 29 | cookie, 30 | }) 31 | 32 | await reply.send( 33 | years.map((year) => { 34 | return { Name: year.Name, Id: year.Id, isActual: year.Data.IsActual } 35 | }) 36 | ) 37 | } 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /apps/api/src/routes/login/captchaRefresh/index.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch" 2 | 3 | export default async function (fastify) { 4 | fastify.get( 5 | "", 6 | { 7 | schema: { 8 | querystring: fastify.getSchema("domain"), 9 | headers: fastify.getSchema("token"), 10 | response: { 11 | 200: { 12 | type: "object", 13 | properties: { 14 | captcha: { type: "string" }, 15 | token: { type: "string" }, 16 | }, 17 | }, 18 | }, 19 | tags: ["login"], 20 | }, 21 | }, 22 | async (req, reply) => { 23 | const { cookies } = req 24 | 25 | const response = await fetch( 26 | `https://sms.${req.query.city}.nis.edu.kz/root/Account/RefreshCaptcha`, 27 | { headers: { cookie: cookies } } 28 | ).then((res) => res.json()) 29 | 30 | if (!response.data?.base64img) 31 | return reply 32 | .code(400) 33 | .send({ message: response.message || "Что-то пошло не так" }) 34 | 35 | return reply.code(200).send({ captcha: response.data.base64img }) 36 | } 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /apps/web-mars/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import { VitePWA } from "vite-plugin-pwa" 3 | import vue from "@vitejs/plugin-vue" 4 | import windi from "vite-plugin-windicss" 5 | 6 | export default defineConfig({ 7 | server: { 8 | port: 3000, 9 | }, 10 | preview: { 11 | port: 8080, 12 | }, 13 | plugins: [ 14 | vue(), 15 | windi(), 16 | VitePWA({ 17 | includeAssets: ["favicon.ico", "robots.txt", "apple-touch-icon.png"], 18 | registerType: "autoUpdate", 19 | manifest: { 20 | name: "enis2", 21 | short_name: "enis2", 22 | description: 23 | "Удобный, быстрый, адаптивный клиент для школьного журнала", 24 | theme_color: "#4885fb", 25 | background_color: "#ffffff", 26 | display: "standalone", 27 | icons: [ 28 | { 29 | src: "icon-192.png", 30 | sizes: "192x192", 31 | type: "image/png", 32 | }, 33 | { 34 | src: "icon-512.png", 35 | sizes: "512x512", 36 | type: "image/png", 37 | }, 38 | ], 39 | }, 40 | }), 41 | ], 42 | }) 43 | -------------------------------------------------------------------------------- /apps/api/src/plugins/handlers/errorHandler.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | 3 | const plugin = fp(async function plugin(fastify) { 4 | fastify.setErrorHandler((err, req, reply) => { 5 | const { validation, code, message } = err 6 | 7 | if (validation) { 8 | const message = validate(err) 9 | return reply.code(400).send({ message }) 10 | } 11 | 12 | if (code === "ETIMEDOUT" || code === 503) 13 | return reply.code(503).send({ message: "Попробуйте снова" }) 14 | 15 | if (code && typeof code === "number" && code > 200 && code < 600) 16 | return reply.code(code).send({ message }) 17 | 18 | reply.code(500).send({ message: "Сервис временно недоступен" }) 19 | console.log(err) 20 | }) 21 | }) 22 | 23 | const validate = ({ validationContext, validation }) => { 24 | const result = validation[0] 25 | const errorVar = result.dataPath ? result.dataPath.substring(1) : "" 26 | const message = `${errorVar} ${result.message}` 27 | 28 | switch (validationContext) { 29 | case "querystring": 30 | return `Invalid query parameters: ${message}` 31 | case "params": 32 | return `Invalid ${errorVar}` 33 | case "body": 34 | return `Invalid body: ${message}` 35 | default: 36 | return "Перезайдите" 37 | } 38 | } 39 | 40 | export default plugin 41 | -------------------------------------------------------------------------------- /apps/web-mars/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-mars", 3 | "version": "0.0.0", 4 | "description": "enis2 web app (mars)", 5 | "license": "MIT", 6 | "author": "Alex Sehl ", 7 | "scripts": { 8 | "dev": "vite", 9 | "start": "vite preview", 10 | "build": "cross-env NODE_ENV=development vite build && mv dist ../../web-build" 11 | }, 12 | "prettier": { 13 | "semi": false 14 | }, 15 | "dependencies": { 16 | "@enis2/shared": "*", 17 | "@vueuse/core": "^9.3.1", 18 | "axios": "^0.27.2", 19 | "maska": "^1.5.0", 20 | "nanoevents": "^7.0.1", 21 | "nanoid": "^4.0.0", 22 | "pinia": "^2.0.23", 23 | "slimeform": "^0.6.1", 24 | "v-wave": "^1.5.0", 25 | "vue": "^3.2.41", 26 | "vue-scroll-picker": "^1.1.3", 27 | "vue-slider-component": "^4.0.0-beta.9", 28 | "vue3-carousel": "^0.1.48" 29 | }, 30 | "devDependencies": { 31 | "@iconify/vue": "^3.2.1", 32 | "@vitejs/plugin-vue": "^3.1.2", 33 | "@vue/eslint-config-prettier": "^7.1.0", 34 | "eslint": "^8.39.0", 35 | "eslint-plugin-vue": "^9.11.0", 36 | "prettier": "^2.8.8", 37 | "vite": "^3.1.8", 38 | "vite-plugin-pwa": "^0.13.1", 39 | "vite-plugin-windicss": "^1.8.8", 40 | "vue-eslint-parser": "^9.3.0", 41 | "windicss": "^3.5.6", 42 | "workbox-window": "^6.5.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 48 | 49 | 57 | -------------------------------------------------------------------------------- /apps/api/src/index.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url" 2 | import { dirname, join } from "path" 3 | import fastify from "fastify" 4 | import autoload from "@fastify/autoload" 5 | import { PORT, JWT_SECRET } from "./config/index.js" 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)) 8 | 9 | const app = fastify({ trustProxy: true }) 10 | 11 | app.register(import("@fastify/cors"), { 12 | origin: "*", 13 | credentials: true, 14 | }) 15 | 16 | app.register(import("@fastify/swagger"), { 17 | routePrefix: "/docs", 18 | swagger: { 19 | info: { 20 | title: "enis2", 21 | description: "enis2 API documentation", 22 | }, 23 | }, 24 | uiConfig: { 25 | deepLinking: true, 26 | docExpansion: "none", 27 | displayRequestDuration: true, 28 | }, 29 | exposeRoute: true, 30 | }) 31 | 32 | console.log(`Docs on: http://localhost:${PORT}/docs`) 33 | 34 | app.register(import("@fastify/compress")) 35 | 36 | app.register(autoload, { dir: join(__dirname, "schema") }) 37 | app.register(autoload, { dir: join(__dirname, "plugins") }) 38 | app.register(autoload, { 39 | dir: join(__dirname, "routes"), 40 | routeParams: true, 41 | }) 42 | 43 | app.register(import("@fastify/jwt"), { secret: JWT_SECRET }) 44 | 45 | app.listen({ port: PORT, host: "0.0.0.0" }, (err) => { 46 | if (err) return console.log(err) 47 | console.info(`App is alive on port ${PORT}`) 48 | }) 49 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/loader.js: -------------------------------------------------------------------------------- 1 | import { ref, computed } from "vue" 2 | import { defineStore } from "pinia" 3 | import { ENDPOINTS } from "../config" 4 | import useGrades from "./grades.js" 5 | import useDiary from "./diary.js" 6 | 7 | export default defineStore("loader", () => { 8 | const gradesStore = useGrades() 9 | const diaryStore = useDiary() 10 | 11 | const existsContent = computed(() => { 12 | return diaryStore.currentDiary.exists || gradesStore.currentGrade.exists 13 | }) 14 | 15 | const loadingQueue = ref([]) 16 | const errors = ref([]) 17 | 18 | const isLoading = computed(() => loadingQueue.value.length > 0) 19 | const loadingEndpoint = computed(() => { 20 | const endpoint = loadingQueue.value[loadingQueue.value.length - 1] 21 | return ENDPOINTS[endpoint?.key] ?? null 22 | }) 23 | 24 | const overlay = computed(() => { 25 | const mode = { 26 | show: loadingQueue.value.some((item) => { 27 | return ENDPOINTS[item.key]?.overlay === "show" 28 | }), 29 | hide: loadingEndpoint.value?.overlay === "hide", 30 | optional: !existsContent.value, 31 | } 32 | return { 33 | active: isLoading.value && !mode.hide, 34 | blocking: isLoading.value && (mode.show || mode.optional), 35 | } 36 | }) 37 | 38 | return { 39 | loadingQueue, 40 | errors, 41 | loadingEndpoint, 42 | isLoading, 43 | overlay, 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/subject.js: -------------------------------------------------------------------------------- 1 | import { ref, reactive, computed } from "vue" 2 | import { defineStore } from "pinia" 3 | import { getSubject } from "../api" 4 | import { getPercent } from "../utils" 5 | 6 | export default defineStore("subject", () => { 7 | const initialState = { 8 | customSections: { 9 | SAU: [], 10 | SAT: [], 11 | }, 12 | originalSections: { 13 | SAU: [], 14 | SAT: [], 15 | }, 16 | originalSubject: {}, 17 | } 18 | const subject = reactive({ ...initialState }) 19 | const GM = ref(false) 20 | 21 | const customSubject = computed(() => { 22 | return { 23 | ...subject.originalSubject, 24 | Score: getPercent(subject.customSections.SAU, subject.customSections.SAT), 25 | } 26 | }) 27 | 28 | const clearSubject = () => { 29 | Object.assign(subject, initialState) 30 | GM.value = false 31 | } 32 | 33 | const fetchSubject = async (subj) => { 34 | subject.originalSubject = subj 35 | try { 36 | const [SAU, SAT] = await getSubject(subj.JournalId, subj.Evaluations) 37 | const sections = { SAU, SAT } 38 | subject.originalSections = sections 39 | subject.customSections = JSON.parse(JSON.stringify(sections)) 40 | } catch (error) { 41 | return Promise.reject(error) 42 | } 43 | } 44 | 45 | return { 46 | subject, 47 | GM, 48 | customSubject, 49 | fetchSubject, 50 | clearSubject, 51 | } 52 | }) 53 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/loaders/LoadingDots.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 56 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/Image.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 51 | 52 | 67 | -------------------------------------------------------------------------------- /packages/shared/src/current-quarter.ts: -------------------------------------------------------------------------------- 1 | export const getCurrentQuarter = () => { 2 | const oneDay = 1000 * 60 * 60 * 24 3 | const now = new Date() 4 | 5 | const start = new Date(now.getFullYear(), 0, 0).getTime() 6 | 7 | const day = Math.floor((now.getTime() - start) / oneDay) 8 | 9 | const firstQuarterEndDate = new Date(new Date(now.setMonth(10)).setDate(6)) 10 | const secondQuarterEndDate = new Date(new Date(now.setMonth(0)).setDate(9)) 11 | const thirdQuarterEndDate = new Date(new Date(now.setMonth(2)).setDate(27)) 12 | const fourthQuarterEndDate = new Date(new Date(now.setMonth(8)).setDate(15)) 13 | 14 | const firstQuarterEndDiff = firstQuarterEndDate.getTime() - start 15 | const secondQuarterEndDiff = secondQuarterEndDate.getTime() - start 16 | const thirdQuarterEndDiff = thirdQuarterEndDate.getTime() - start 17 | const fourthQuarterEndDiff = fourthQuarterEndDate.getTime() - start 18 | 19 | const firstQuarterEnd = Math.floor(firstQuarterEndDiff / oneDay) 20 | const secondQuarterEnd = Math.floor(secondQuarterEndDiff / oneDay) 21 | const thirdQuarterEnd = Math.floor(thirdQuarterEndDiff / oneDay) 22 | const fourthQuarterEnd = Math.floor(fourthQuarterEndDiff / oneDay) 23 | 24 | if (day > fourthQuarterEnd && day <= firstQuarterEnd) return 1 25 | if (day > firstQuarterEnd || day <= secondQuarterEnd) return 2 26 | if (day > secondQuarterEnd && day <= thirdQuarterEnd) return 3 27 | if (day > thirdQuarterEnd && day <= fourthQuarterEnd) return 4 28 | 29 | return 1 30 | } 31 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/notifications/Notifications.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # enis2 2 | 3 | [![Uptime Robot](https://img.shields.io/uptimerobot/status/m788722189-0972bdac9b2e03392769f154?label=server)](https://stats.uptimerobot.com/kXD0runRnw/788722189) 4 | 5 | One of many _enis_ implementations. 6 | 7 | > _enis_ is a common name references unofficial client for an electronic school journal used in NIS schools. 8 | 9 | [approximate history](https://wsehl.notion.site/wsehl/enis2-docs-3a033e48f5c94eb7aa153bd3c103d729) 10 | 11 | ## Known Issues 12 | 13 | As it is described [here](https://github.com/superhooman/enis-proxy), NIS doesn't have a public API, so we had to do a custom server that works as an interlayer to bypass the CORS policy since the approach with a proxy server doesn't seem to work anymore. That's why it leads to additional delays and privacy concerns. 14 | 15 | ## Getting Started 16 | 17 | This project requires [Node.js](https://nodejs.org/en/download/current/) 16+ and [yarn](https://yarnpkg.com/) 18 | 19 | ### Copy env template file 20 | 21 | ```bash 22 | cp .env.development .env 23 | ``` 24 | 25 | ### Install 26 | 27 | ```bash 28 | yarn install 29 | ``` 30 | 31 | ### Start 32 | 33 | ```bash 34 | yarn dev 35 | ``` 36 | 37 | ## API documentation 38 | 39 | Starting the server will let you investigate the API via Swagger by getting detailed information about endpoints and their request/response schemas at [http://localhost:4000/docs](http://localhost:4000/docs) 40 | 41 | Swagger 42 | 43 | ## Contributing 44 | 45 | - Fork the repo and create your branch from main 46 | - Submit the pull request 47 | 48 | ## License 49 | 50 | [MIT](/LICENSE) 51 | -------------------------------------------------------------------------------- /apps/api/src/routes/dashboard/terms/_yearID/index.js: -------------------------------------------------------------------------------- 1 | import { URLSearchParams } from "url" 2 | import { getCurrentQuarter } from "@enis2/shared" 3 | 4 | export default async function (fastify) { 5 | fastify.get( 6 | "", 7 | { 8 | schema: { 9 | querystring: fastify.getSchema("domain"), 10 | headers: fastify.getSchema("token"), 11 | params: { 12 | type: "object", 13 | required: ["yearID"], 14 | properties: { 15 | yearID: { type: "string", minLength: 36, maxLength: 36 }, 16 | }, 17 | }, 18 | response: { 19 | 200: { 20 | type: "array", 21 | items: { 22 | type: "object", 23 | properties: { 24 | Name: { type: "string" }, 25 | Id: { type: "string" }, 26 | isActual: { type: "boolean", default: false }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | tags: ["dashboard"], 32 | }, 33 | }, 34 | async (req, reply) => { 35 | const cookie = req.cookies 36 | 37 | const params = new URLSearchParams() 38 | params.append("schoolYearId", req.params.yearID) 39 | 40 | const periods = await fastify.api({ 41 | method: "POST", 42 | url: `https://sms.${req.query.city}.nis.edu.kz/Ref/GetPeriods`, 43 | body: params, 44 | cookie, 45 | }) 46 | 47 | const sortedPeriods = periods.data.sort((a, b) => { 48 | if (a.Name < b.Name) return -1; 49 | if (a.Name > b.Name) return 1; 50 | return 0; 51 | }); 52 | 53 | sortedPeriods[getCurrentQuarter() - 1].isActual = true; 54 | await reply.send(sortedPeriods); 55 | } 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/loaders/LoadingOverlay.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 | 79 | -------------------------------------------------------------------------------- /apps/web-mars/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 49 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/grades.js: -------------------------------------------------------------------------------- 1 | import { computed } from "vue" 2 | import { useStorage } from "@vueuse/core" 3 | import { defineStore } from "pinia" 4 | import { getGrades } from "../api" 5 | import { findIndex, findItem } from "../utils" 6 | import useSettingsStore from "./settings.js" 7 | import useYearsStore from "./years.js" 8 | 9 | export default defineStore("grades", () => { 10 | const yearsStore = useYearsStore() 11 | const settingsStore = useSettingsStore() 12 | 13 | const gradesData = useStorage("gradesData", []) 14 | 15 | const grades = computed(() => { 16 | const matchedGrades = findItem(gradesData.value, { 17 | yearName: settingsStore.settings.year, 18 | }) 19 | return matchedGrades ? matchedGrades.grades : [] 20 | }) 21 | 22 | const currentGrade = computed(() => { 23 | return findIndex(gradesData.value, { 24 | yearName: settingsStore.settings.year, 25 | }) 26 | }) 27 | 28 | const clearGrades = () => { 29 | gradesData.value = [] 30 | } 31 | 32 | const fetchGrades = async (force = false) => { 33 | const yearId = yearsStore.currentYearId 34 | const yearName = settingsStore.settings.year 35 | 36 | const { index, exists } = findIndex(gradesData.value, { yearName }) 37 | 38 | if (exists && !force) return 39 | 40 | try { 41 | const data = await getGrades(yearId) 42 | if (exists) { 43 | gradesData.value[index].grades = data 44 | } else { 45 | const gradeObject = { 46 | grades: data, 47 | yearName: yearName, 48 | } 49 | gradesData.value = [...gradesData.value, gradeObject] 50 | } 51 | } catch (error) { 52 | return Promise.reject(error) 53 | } 54 | } 55 | 56 | return { 57 | gradesData, 58 | grades, 59 | currentGrade, 60 | fetchGrades, 61 | clearGrades, 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /apps/web-mars/src/utils/index.js: -------------------------------------------------------------------------------- 1 | const getSum = (array, query) => { 2 | return array.reduce((t, c) => t + c[query], 0) 3 | } 4 | 5 | export const getSectionsScores = (sections) => { 6 | const filteredSections = sections.filter((s) => s.Score !== -1) 7 | return { 8 | score: getSum(filteredSections, "Score"), 9 | max: getSum(filteredSections, "MaxScore"), 10 | } 11 | } 12 | 13 | export const formatPercent = (percent) => { 14 | return Number(percent ? percent.toFixed(2) : 0).toString() 15 | } 16 | 17 | export const getPercent = (SAU, SAT) => { 18 | const { score: SAUscores, max: SAUmaxScores } = getSectionsScores(SAU) 19 | const { score: SATscores, max: SATmaxScores } = getSectionsScores(SAT) 20 | const SAUpart = SAUscores / (2 * SAUmaxScores) 21 | const SATpart = SATscores / (2 * SATmaxScores) 22 | return formatPercent((SAUpart + SATpart) * 100) 23 | } 24 | 25 | export const getPercentDecimals = (percent) => { 26 | const [before, after] = `${percent}`.split(".") 27 | return { before, after } 28 | } 29 | 30 | export const getRandomItem = (array) => { 31 | return array[Math.floor(Math.random() * array.length)] 32 | } 33 | 34 | const findByQuery = (i, query) => { 35 | return Object.entries(query).every(([k, v]) => i[k].toString().includes(v)) 36 | } 37 | 38 | export const findItem = (array, query) => { 39 | return array.find((i) => findByQuery(i, query)) 40 | } 41 | 42 | export const findIndex = (array, query) => { 43 | const idx = array.findIndex((i) => findByQuery(i, query)) 44 | const index = idx === -1 ? null : idx 45 | const exists = index !== null 46 | return { index, exists } 47 | } 48 | 49 | export const between = (x, min, max) => { 50 | return x >= min && x <= max 51 | } 52 | 53 | export const isRequired = (value) => { 54 | if (value && value.trim()) { 55 | return true 56 | } 57 | return "Required" 58 | } 59 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/Switch.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | 81 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/settings.js: -------------------------------------------------------------------------------- 1 | import { computed } from "vue" 2 | import { useStorage } from "@vueuse/core" 3 | import { defineStore } from "pinia" 4 | import { getCity } from "../api" 5 | import { DEFAULT_RANGES, SCHOOLS } from "../config" 6 | 7 | export default defineStore("settings", () => { 8 | const initialState = { 9 | tab: "", 10 | year: "", 11 | theme: "dark", 12 | school: "", 13 | rememberMe: false, 14 | sortBy: "score", 15 | hideEmpty: false, 16 | } 17 | const settings = useStorage("settings", { ...initialState }) 18 | const ranges = useStorage("customRanges", [...DEFAULT_RANGES]) 19 | 20 | const darkTheme = computed({ 21 | get: () => settings.value.theme === "dark", 22 | set: (value) => { 23 | settings.value.theme = value ? "dark" : "light" 24 | }, 25 | }) 26 | 27 | const clearSettings = () => { 28 | Object.assign( 29 | settings.value, 30 | // eslint-disable-next-line no-unused-vars 31 | (({ theme, school, ...o }) => o)(initialState) 32 | ) 33 | } 34 | 35 | const toggleTheme = () => { 36 | settings.value.theme = settings.value.theme === "light" ? "dark" : "light" 37 | } 38 | const predictSchool = async () => { 39 | if (settings.value.school) return 40 | try { 41 | const { city, region } = await getCity() 42 | const predictedSchool = SCHOOLS.find((item) => { 43 | return ( 44 | item.city === city || 45 | city.includes(item.city) || 46 | region.includes(item.city) 47 | ) 48 | }) 49 | if (city && predictedSchool) { 50 | settings.value.school = predictedSchool.value 51 | } 52 | } catch (error) { 53 | return Promise.reject(error) 54 | } 55 | } 56 | 57 | return { 58 | toggleTheme, 59 | clearSettings, 60 | predictSchool, 61 | settings, 62 | darkTheme, 63 | ranges, 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/modal-containers/SubjectContainer.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/tabs/Tab.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | 57 | 84 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/modal-containers/AvailabilityContainer.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 49 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/ScrollPicker.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 30 | 31 | 97 | -------------------------------------------------------------------------------- /packages/shared/src/config.ts: -------------------------------------------------------------------------------- 1 | const SCHOOLS = [ 2 | { 3 | value: "akt", 4 | label: "Актау ХБН", 5 | city: "Aktau", 6 | }, 7 | { 8 | value: "akb", 9 | label: "Актобе ФМН", 10 | city: "Aktobe", 11 | }, 12 | { 13 | value: "fmalm", 14 | label: "Алматы ФМН", 15 | city: "Almaty", 16 | }, 17 | { 18 | value: "hbalm", 19 | label: "Алматы ХБН", 20 | city: "Almaty", 21 | }, 22 | { 23 | value: "ast", 24 | label: "Астана ФМН", 25 | city: "Nur-Sultan", 26 | }, 27 | { 28 | value: "atr", 29 | label: "Атырау ХБН", 30 | city: "Atyrau", 31 | }, 32 | { 33 | value: "krg", 34 | label: "Караганда ХБН", 35 | city: "Karagandy", 36 | }, 37 | { 38 | value: "kt", 39 | label: "Кокшетау ФМН", 40 | city: "Kokshetau", 41 | }, 42 | { 43 | value: "kst", 44 | label: "Костанай ФМН", 45 | city: "Kostanay", 46 | }, 47 | { 48 | value: "kzl", 49 | label: "Кызылорда ХБН", 50 | city: "Kyzylorda", 51 | }, 52 | { 53 | value: "pvl", 54 | label: "Павлодар ХБН", 55 | city: "Pavlodar", 56 | }, 57 | { 58 | value: "ptr", 59 | label: "Петропавловск ХБН", 60 | city: "Petropavl", 61 | }, 62 | { 63 | value: "sm", 64 | label: "Семей ФМН", 65 | city: "Semey", 66 | }, 67 | { 68 | value: "tk", 69 | label: "Талдыкорган ФМН", 70 | city: "Taldykorgan", 71 | }, 72 | { 73 | value: "trz", 74 | label: "Тараз ФМН", 75 | city: "Taraz", 76 | }, 77 | { 78 | value: "ura", 79 | label: "Уральск ФМН", 80 | city: "Uralsk", 81 | }, 82 | { 83 | value: "ukk", 84 | label: "Усть-Каменогорск ХБН", 85 | city: "Ust-Kamenogorsk", 86 | }, 87 | { 88 | value: "fmsh", 89 | label: "Шымкент ФМН", 90 | city: "Shymkent", 91 | }, 92 | { 93 | value: "hbsh", 94 | label: "Шымкент ХБН", 95 | city: "Shymkent", 96 | }, 97 | { 98 | value: "trk", 99 | label: "Туркестан ХБН", 100 | city: "Turkistan", 101 | }, 102 | ] as const 103 | 104 | export { SCHOOLS } 105 | -------------------------------------------------------------------------------- /apps/api/src/routes/dashboard/subject/rubrics/index.js: -------------------------------------------------------------------------------- 1 | import { URLSearchParams } from "url" 2 | 3 | export default async function (fastify) { 4 | fastify.get( 5 | "", 6 | { 7 | schema: { 8 | headers: fastify.getSchema("token"), 9 | querystring: { 10 | type: "object", 11 | required: ["sectionId", "rubricId", "city"], 12 | properties: { 13 | sectionId: { type: "string", minLength: 36, maxLength: 36 }, 14 | rubricId: { type: "string", minLength: 36, maxLength: 36 }, 15 | city: fastify.getSchema("city"), 16 | }, 17 | }, 18 | response: { 19 | 200: { 20 | type: "array", 21 | items: { 22 | type: "object", 23 | properties: { 24 | criterion: { type: "string" }, 25 | descriptors: { 26 | type: "array", 27 | items: { type: "string" }, 28 | }, 29 | resultId: { type: "number" }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | tags: ["dashboard"], 35 | }, 36 | }, 37 | async (req, reply) => { 38 | const params = new URLSearchParams() 39 | params.append("sectionId", req.query.sectionId) 40 | params.append("rubricId", req.query.rubricId) 41 | 42 | const cookie = req.cookies 43 | 44 | const response = await fastify.api({ 45 | url: `https://sms.${req.query.city}.nis.edu.kz/Jce/Diary/GetRubricResults`, 46 | method: "POST", 47 | body: params, 48 | cookie, 49 | }) 50 | 51 | const resultTypes = ["HighResult", "MediumResult", "LowResult"] 52 | 53 | response.data.forEach((criterion) => { 54 | criterion.resultId = resultTypes.findIndex((type) => criterion[type]) 55 | criterion.criterion = criterion.Criterion 56 | criterion.descriptors = [ 57 | criterion.HighDescriptor, 58 | criterion.MediumDescriptor, 59 | criterion.LowDescriptor, 60 | ] 61 | }) 62 | 63 | await reply.send(response.data) 64 | } 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /apps/api/src/plugins/api.js: -------------------------------------------------------------------------------- 1 | import fp from "fastify-plugin" 2 | import fetch from "node-fetch" 3 | import { FAKE_USER_AGENT } from "../config/index.js" 4 | 5 | const unauthorizedErrorMessages = [ 6 | "Сессия пользователя была завершена, перезагрузите страницу", 7 | "Время работы с дневником завершено. Для продолжения необходимо обновить модуль", 8 | ] 9 | 10 | const isUnauthorizedErrorMessage = (message) => { 11 | return unauthorizedErrorMessages.indexOf(message) !== -1 12 | } 13 | 14 | export default fp(async function plugin(fastify) { 15 | fastify.decorate( 16 | "api", 17 | async ({ cookie = "", body = {}, url, method = "GET" }) => { 18 | let options = { 19 | method, 20 | headers: { cookie, "user-agent": FAKE_USER_AGENT }, 21 | } 22 | 23 | if (method === "POST") options = Object.assign(options, { body }) 24 | 25 | const response = await fetch(url, options) 26 | 27 | if (!response.ok) { 28 | const err = new Error(response.statusText) 29 | err.code = response.status 30 | throw err 31 | } 32 | 33 | const isJSON = 34 | response.headers.raw()["content-type"][0] === "text/json; charset=utf-8" 35 | 36 | if (!isJSON) { 37 | const message = await response.text() 38 | 39 | if (isUnauthorizedErrorMessage(message)) { 40 | const err = new Error("Сессия пользователя была завершена") 41 | err.code = 401 42 | throw err 43 | } 44 | 45 | const err = new Error(message) 46 | err.code = 400 47 | throw err 48 | } 49 | 50 | const json = await response.json() 51 | 52 | if (!json.success) { 53 | if (isUnauthorizedErrorMessage(json.message)) { 54 | const err = new Error("Время работы с дневником завершено") 55 | err.code = 401 56 | throw err 57 | } 58 | 59 | const err = new Error(json.details || json.message) 60 | err.code = 400 61 | throw err 62 | } 63 | 64 | json.statusCode = response.status 65 | 66 | json.cookie = fastify.cookieParse(response) 67 | 68 | return json 69 | } 70 | ) 71 | }) 72 | -------------------------------------------------------------------------------- /apps/api/src/routes/dashboard/subject/index.js: -------------------------------------------------------------------------------- 1 | import { URLSearchParams } from "url" 2 | 3 | export default async function (fastify) { 4 | fastify.get( 5 | "", 6 | { 7 | schema: { 8 | headers: fastify.getSchema("token"), 9 | querystring: { 10 | type: "object", 11 | required: ["journalId", "evaluations[]", "city"], 12 | properties: { 13 | journalId: { type: "string", minLength: 36, maxLength: 36 }, 14 | "evaluations[]": { 15 | type: "array", 16 | items: { type: "string", minLength: 36, maxLength: 36 }, 17 | }, 18 | city: fastify.getSchema("city"), 19 | }, 20 | }, 21 | response: { 22 | 200: { 23 | type: "array", 24 | items: { 25 | type: "array", 26 | items: { 27 | type: "object", 28 | properties: { 29 | Name: { type: "string" }, 30 | Score: { type: "number" }, 31 | MaxScore: { type: "number" }, 32 | SectionId: { type: "string" }, 33 | RubricId: { type: "string" }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | }, 39 | tags: ["dashboard"], 40 | }, 41 | }, 42 | async (req, reply) => { 43 | const evaluations = req.query["evaluations[]"] 44 | 45 | const createSubjectPromise = async (evalId) => { 46 | const params = new URLSearchParams() 47 | params.append("journalId", req.query.journalId) 48 | params.append("evalId", evalId) 49 | 50 | const cookie = req.cookies 51 | const response = await fastify.api({ 52 | url: `https://sms.${req.query.city}.nis.edu.kz/Jce/Diary/GetResultByEvalution`, 53 | method: "POST", 54 | body: params, 55 | cookie, 56 | }) 57 | response.data.forEach((item) => (item.SectionId = item.Id)) 58 | return response.data 59 | } 60 | 61 | const subject = await Promise.all([ 62 | createSubjectPromise(evaluations[0]), 63 | createSubjectPromise(evaluations[1]), 64 | ]) 65 | 66 | await reply.send(subject) 67 | } 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/auth.js: -------------------------------------------------------------------------------- 1 | import { ref } from "vue" 2 | import { useStorage } from "@vueuse/core" 3 | import { defineStore } from "pinia" 4 | import { login as _login, refreshCaptcha } from "../api" 5 | import { notify } from "../services/notify.js" 6 | import useYearsStore from "./years.js" 7 | import useTermsStore from "./terms.js" 8 | import useDiaryStore from "./diary.js" 9 | import useGradesStore from "./grades.js" 10 | import useSettingsStore from "./settings.js" 11 | import useSubjectStore from "./subject.js" 12 | 13 | export default defineStore("auth", () => { 14 | const { clearYears } = useYearsStore() 15 | const { clearTerms } = useTermsStore() 16 | const { clearDiary } = useDiaryStore() 17 | const { clearGrades } = useGradesStore() 18 | const { clearSettings } = useSettingsStore() 19 | const { clearSubject } = useSubjectStore() 20 | 21 | const token = useStorage("token", "") 22 | const authenticated = useStorage("authenticated", false) 23 | 24 | const captcha = ref(null) 25 | 26 | const clearStore = () => { 27 | clearYears() 28 | clearTerms() 29 | clearDiary() 30 | clearGrades() 31 | clearSettings() 32 | clearSubject() 33 | } 34 | 35 | const setToken = (newToken) => { 36 | token.value = newToken 37 | } 38 | 39 | const login = async (credentials) => { 40 | try { 41 | const data = await _login(credentials) 42 | setToken(data.token) 43 | captcha.value = null 44 | authenticated.value = true 45 | } catch (error) { 46 | authenticated.value = false 47 | setToken(error.response.data.token) 48 | notify.show({ 49 | type: "danger", 50 | message: "Произошла ошибка, попробуйте войти в СУШ", 51 | }) 52 | return Promise.reject(error) 53 | } 54 | } 55 | const logout = () => { 56 | token.value = null 57 | authenticated.value = false 58 | clearStore() 59 | } 60 | const updateCaptcha = async () => { 61 | try { 62 | const data = await refreshCaptcha() 63 | 64 | captcha.value = data.captcha 65 | } catch (error) { 66 | return Promise.reject(error) 67 | } 68 | } 69 | return { 70 | token, 71 | captcha, 72 | authenticated, 73 | login, 74 | logout, 75 | updateCaptcha, 76 | setToken, 77 | } 78 | }) 79 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/years.js: -------------------------------------------------------------------------------- 1 | import { computed } from "vue" 2 | import { useStorage } from "@vueuse/core" 3 | import { defineStore, storeToRefs } from "pinia" 4 | import { getYears } from "../api" 5 | import { findItem } from "../utils" 6 | import useSettingsStore from "./settings.js" 7 | 8 | export default defineStore("years", () => { 9 | const settingsStore = useSettingsStore() 10 | const { settings } = storeToRefs(settingsStore) 11 | 12 | const years = useStorage("yearsData", []) 13 | const actual = useStorage("actualYearName", null) 14 | 15 | const currentYearId = computed(() => { 16 | const matchedYear = findItem(years.value, { label: settings.value.year }) 17 | return matchedYear ? matchedYear.value : "" 18 | }) 19 | 20 | const clearYears = () => { 21 | years.value = [] 22 | actual.value = null 23 | } 24 | 25 | const shorterYearName = (name) => { 26 | return name.substring(0, 9) 27 | } 28 | 29 | const fetchYears = async (force = false) => { 30 | const exists = years.value.length 31 | 32 | if (exists && !force) return 33 | 34 | try { 35 | const data = await getYears() 36 | 37 | const reversedYears = data.reverse() // 2022, 2021 => 2021, 2022 38 | const formattedYears = reversedYears.map( 39 | ({ Id: value, Name: label, isActual }) => ({ 40 | value, 41 | label: shorterYearName(label), 42 | isActual, 43 | }) 44 | ) 45 | const actualYearIndex = data.findIndex((year) => year.isActual) 46 | const firstYearIndex = formattedYears.findIndex((year) => { 47 | /* 48 | (hardcoded) 49 | The first year that started to work with the new data format 50 | It is impossible to see marks for previous years 51 | */ 52 | return year.label === "2019-2020" 53 | }) 54 | years.value = formattedYears.slice(firstYearIndex, actualYearIndex + 2) 55 | 56 | actual.value = formattedYears.find((year) => year.isActual).label 57 | settings.value.year = settings.value.year || actual.value 58 | } catch (error) { 59 | return Promise.reject(error) 60 | } 61 | } 62 | 63 | return { 64 | years, 65 | actualYearName: actual, 66 | currentYearId, 67 | fetchYears, 68 | clearYears, 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/Select.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 87 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/terms.js: -------------------------------------------------------------------------------- 1 | import { computed } from "vue" 2 | import { useStorage } from "@vueuse/core" 3 | import { defineStore, storeToRefs } from "pinia" 4 | import { getTerms } from "../api" 5 | import { findIndex, findItem } from "../utils" 6 | import useSettingsStore from "./settings.js" 7 | import useYearsStore from "./years.js" 8 | 9 | export default defineStore("terms", () => { 10 | const yearsStore = useYearsStore() 11 | const settingsStore = useSettingsStore() 12 | const { settings } = storeToRefs(settingsStore) 13 | 14 | const termsData = useStorage("termsData", []) 15 | const actual = useStorage("actualTermName", null) 16 | 17 | const matchedTerm = computed(() => { 18 | return findItem(termsData.value, { yearName: settings.value.year }) 19 | }) 20 | 21 | const currentTermId = computed(() => { 22 | const termData = 23 | matchedTerm.value && 24 | findItem(matchedTerm.value.terms, { Name: settings.value.tab }) 25 | return termData ? termData.Id : "" 26 | }) 27 | 28 | const terms = computed(() => { 29 | return matchedTerm.value 30 | ? matchedTerm.value.terms 31 | : [{ Name: "1" }, { Name: "2" }, { Name: "3" }, { Name: "4" }] 32 | }) 33 | 34 | const clearTerms = () => { 35 | termsData.value = [] 36 | actual.value = null 37 | } 38 | 39 | const shorterTermName = (name) => { 40 | return name.substring(0, 1) 41 | } 42 | 43 | const fetchTerms = async (force = false) => { 44 | const yearId = yearsStore.currentYearId 45 | const yearName = settings.value.year 46 | 47 | const { index, exists } = findIndex(termsData.value, { yearName }) 48 | 49 | if (exists && !force) return 50 | 51 | try { 52 | const terms = await getTerms(yearId) 53 | 54 | const actualTerm = terms.find((term) => term.isActual) 55 | 56 | actual.value = shorterTermName(actualTerm.Name) 57 | settings.value.tab = settings.value.tab || actual.value 58 | 59 | const formattedTerms = terms.map(({ Id, Name: label }) => ({ 60 | Id, 61 | Name: shorterTermName(label), 62 | })) 63 | 64 | if (exists) { 65 | termsData.value[index].terms = formattedTerms 66 | } else { 67 | const termObject = { 68 | yearName: yearName, 69 | terms: formattedTerms, 70 | } 71 | termsData.value = [...termsData.value, termObject] 72 | } 73 | } catch (error) { 74 | return Promise.reject(error) 75 | } 76 | } 77 | 78 | return { 79 | termsData, 80 | terms, 81 | actualTermName: actual, 82 | currentTermId, 83 | fetchTerms, 84 | clearTerms, 85 | } 86 | }) 87 | -------------------------------------------------------------------------------- /apps/web-mars/src/config/index.js: -------------------------------------------------------------------------------- 1 | import { SCHOOLS } from "@enis2/shared" 2 | 3 | const isDev = import.meta.env.DEV 4 | const isMock = isDev && false 5 | 6 | const SERVER_URL = import.meta.env.VITE_SERVER_URL 7 | 8 | if (!SERVER_URL) throw new Error("VITE_SERVER_URL is not defined") 9 | 10 | const DEFAULT_ERROR_MESSAGE = "Что-то пошло не так" 11 | 12 | const createUrl = (endpoint) => `${SERVER_URL}/${endpoint}` 13 | 14 | const ENDPOINTS = { 15 | HEALTH_SMS: { 16 | endpoint: { 17 | real: createUrl("health/sms"), 18 | mock: "https://run.mocky.io/v3/c6831c53-0201-4cc5-970e-257d4a1a685b", 19 | }, 20 | overlay: "show", 21 | }, 22 | CITY: { 23 | endpoint: { 24 | real: createUrl("city"), 25 | mock: "https://run.mocky.io/v3/fd55269c-ef09-4d06-8400-0888a30cf6ee", 26 | }, 27 | overlay: "hide", 28 | }, 29 | LOGIN: { 30 | endpoint: { 31 | real: createUrl("login"), 32 | mock: "https://run.mocky.io/v3/a1dc1050-84f3-44b5-9564-9d5a2876bf09", 33 | }, 34 | overlay: "show", 35 | }, 36 | REFRESH_CAPTCHA: { 37 | endpoint: { 38 | real: createUrl("login/captchaRefresh"), 39 | mock: "https://run.mocky.io/v3/92769a52-469f-4148-a593-8fe549940b20", 40 | }, 41 | overlay: "hide", 42 | }, 43 | YEARS: { 44 | endpoint: { 45 | real: createUrl("dashboard/years"), 46 | mock: "https://run.mocky.io/v3/252c9bb8-3abd-4741-a4e9-2368514332bf", 47 | }, 48 | overlay: "show", 49 | }, 50 | TERMS: { 51 | endpoint: { 52 | real: createUrl("dashboard/terms/"), 53 | mock: "https://run.mocky.io/v3/", 54 | }, 55 | overlay: "show", 56 | }, 57 | DIARY: { 58 | endpoint: { 59 | real: createUrl("dashboard/diary/"), 60 | mock: "https://run.mocky.io/v3/", 61 | }, 62 | overlay: "optional", 63 | }, 64 | SUBJECT: { 65 | endpoint: { 66 | real: createUrl("dashboard/subject"), 67 | mock: "https://run.mocky.io/v3/2fc47f41-b422-4fa3-9677-fd9013e27f12", 68 | }, 69 | overlay: "hide", 70 | }, 71 | GRADES: { 72 | endpoint: { 73 | real: createUrl("dashboard/grades"), 74 | mock: "https://run.mocky.io/v3/a3da8e69-1e20-48d9-9b2f-f5d98aca8f61", 75 | }, 76 | overlay: "optional", 77 | }, 78 | } 79 | 80 | const DA_LINK = "https://www.donationalerts.com/r/wsehl" 81 | const TG_LINK = "https://t.me/joinchat/ToHSvx2gVOBkMzBi" 82 | const GH_LINK = "https://github.com/anyrange/enis2" 83 | 84 | const DEFAULT_RANGES = [0, 40, 65, 85, 100] 85 | 86 | export { 87 | isDev, 88 | isMock, 89 | SERVER_URL, 90 | DEFAULT_ERROR_MESSAGE, 91 | ENDPOINTS, 92 | SCHOOLS, 93 | DA_LINK, 94 | TG_LINK, 95 | GH_LINK, 96 | DEFAULT_RANGES, 97 | } 98 | -------------------------------------------------------------------------------- /apps/web-mars/src/stores/diary.js: -------------------------------------------------------------------------------- 1 | import { computed } from "vue" 2 | import { useStorage } from "@vueuse/core" 3 | import { defineStore } from "pinia" 4 | import { getDiary } from "../api" 5 | import { findIndex, findItem } from "../utils" 6 | import useSettingsStore from "./settings.js" 7 | import useTermsStore from "./terms.js" 8 | import useAuthStore from "./auth.js" 9 | 10 | export default defineStore("diary", () => { 11 | const settingsStore = useSettingsStore() 12 | 13 | const authStore = useAuthStore() 14 | const termsStore = useTermsStore() 15 | 16 | const diaryData = useStorage("diaryData", []) 17 | 18 | const matchedDiary = computed(() => { 19 | return findItem(diaryData.value, { 20 | yearName: settingsStore.settings.year, 21 | termName: settingsStore.settings.tab, 22 | }) 23 | }) 24 | 25 | const currentDiary = computed(() => { 26 | return findIndex(diaryData.value, { 27 | yearName: settingsStore.settings.year, 28 | termName: settingsStore.settings.tab, 29 | }) 30 | }) 31 | 32 | const diary = computed(() => { 33 | const rawDiary = matchedDiary.value ? matchedDiary.value.diary : [] 34 | 35 | const sort = { 36 | name: (array) => array.sort((a, b) => a.Name.localeCompare(b.Name)), 37 | score: (array) => array.sort((a, b) => b.Score - a.Score), 38 | } 39 | 40 | const sortedDiary = sort[settingsStore.settings.sortBy](rawDiary) 41 | 42 | const filteredDiary = sortedDiary.filter((o) => o.Score !== 0) 43 | const allEmpty = !filteredDiary.length 44 | 45 | return settingsStore.settings.hideEmpty 46 | ? allEmpty 47 | ? sortedDiary 48 | : filteredDiary 49 | : sortedDiary 50 | }) 51 | 52 | const clearDiary = () => { 53 | diaryData.value = [] 54 | } 55 | 56 | const fetchDiary = async (force = false) => { 57 | const termId = termsStore.currentTermId 58 | 59 | const termName = settingsStore.settings.tab 60 | const yearName = settingsStore.settings.year 61 | 62 | const { index, exists } = findIndex(diaryData.value, { 63 | termName, 64 | yearName, 65 | }) 66 | 67 | if (exists && !force) return 68 | 69 | try { 70 | const { data, token } = await getDiary(termId) 71 | authStore.setToken(token) 72 | if (exists) { 73 | diaryData.value[index].diary = data 74 | } else { 75 | const diaryObject = { 76 | diary: data, 77 | termName, 78 | yearName, 79 | } 80 | diaryData.value = [...diaryData.value, diaryObject] 81 | } 82 | } catch (error) { 83 | return Promise.reject(error) 84 | } 85 | } 86 | 87 | return { 88 | diaryData, 89 | diary, 90 | currentDiary, 91 | fetchDiary, 92 | clearDiary, 93 | } 94 | }) 95 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/Modal.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 85 | 86 | 96 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/Input.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 52 | 53 | 109 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/modal-containers/settings/SettingsContainerSlider.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 73 | 74 | 77 | 78 | 107 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/subject/SubjectDiarySection.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 95 | -------------------------------------------------------------------------------- /apps/web-mars/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | enis2 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 37 | 46 | 47 | 79 | 87 | 88 | 89 | 90 |
91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/subject/SubjectDiary.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 99 | 100 | 113 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/modal-containers/settings/SettingsContainer.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 72 | 73 | 106 | -------------------------------------------------------------------------------- /apps/api/src/routes/login/index.js: -------------------------------------------------------------------------------- 1 | import { URLSearchParams } from "url" 2 | import { promisify } from "util" 3 | import fetch from "node-fetch" 4 | import { encrypt, decrypt } from "../../utils/crypto.js" 5 | import { FAKE_USER_AGENT } from "../../config/index.js" 6 | 7 | const getDecryptedPassword = (password) => { 8 | return password?.content ? decrypt(password) : password 9 | } 10 | 11 | export default async function (fastify) { 12 | fastify.post( 13 | "", 14 | { 15 | schema: { 16 | querystring: fastify.getSchema("domain"), 17 | headers: fastify.getSchema("token"), 18 | body: { 19 | type: "object", 20 | properties: { 21 | login: { type: "string", minLength: 12, maxLength: 12 }, 22 | password: { type: "string", minLength: 1 }, 23 | captchaInput: { type: "string" }, 24 | }, 25 | }, 26 | response: { 27 | 200: { 28 | type: "object", 29 | properties: { 30 | message: { type: "string" }, 31 | token: { type: "string" }, 32 | }, 33 | }, 34 | 400: { 35 | type: "object", 36 | properties: { 37 | message: { type: "string" }, 38 | token: { type: "string" }, 39 | data: { 40 | type: "object", 41 | properties: { 42 | base64img: { type: "string" }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | tags: ["login"], 49 | }, 50 | }, 51 | async (req, reply) => { 52 | const { captchaInput } = req.body 53 | const { cookies: userCookies, account } = req 54 | 55 | const mergedCookies = fastify.mergeCookies( 56 | userCookies, 57 | "lang=ru-RU; path=/" 58 | ) 59 | 60 | const login = req.body.login || account?.login 61 | 62 | const password = 63 | req.body.password || getDecryptedPassword(account?.password) 64 | 65 | if (!(login && password)) 66 | return reply 67 | .code(401) 68 | .send({ message: "Neither token nor credentials were provided" }) 69 | 70 | const params = new URLSearchParams() 71 | params.append("login", login) 72 | params.append("password", password) 73 | params.append("captchaInput", captchaInput || "") 74 | params.append("twoFactorAuthCode", "") 75 | params.append("application2FACode", "") 76 | 77 | const res = await fetch( 78 | `https://sms.${req.query.city}.nis.edu.kz/root/Account/LogOn`, 79 | { 80 | method: "POST", 81 | headers: { 82 | cookie: mergedCookies, 83 | "user-agent": FAKE_USER_AGENT, 84 | }, 85 | body: params, 86 | } 87 | ) 88 | const body = await res.json() 89 | 90 | const updatedCookies = fastify.cookieParse(res) 91 | const cookies = fastify.mergeCookies(mergedCookies, updatedCookies) 92 | 93 | const promiseJWT = promisify(fastify.jwt.sign) 94 | body.token = await promiseJWT( 95 | { 96 | cookies, 97 | account: { 98 | login, 99 | password: encrypt(password), 100 | }, 101 | }, 102 | null 103 | ) 104 | 105 | const statusCode = body.success ? 200 : 400 106 | body.data = Object.assign({}, body.data) 107 | 108 | return reply.code(statusCode).send(body) 109 | } 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /apps/web-mars/src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { nanoid } from "nanoid" 3 | import { ENDPOINTS, DEFAULT_ERROR_MESSAGE, isMock } from "../config" 4 | import useLoaderStore from "../stores/loader" 5 | import useAuthStore from "../stores/auth" 6 | import useSettingsStore from "../stores/settings" 7 | 8 | const api = axios.create({ 9 | timeout: 1000 * 30, // 30 seconds 10 | }) 11 | 12 | const findEndpoint = (url) => { 13 | return Object.keys(ENDPOINTS).find((key) => { 14 | const value = ENDPOINTS[key].endpoint 15 | return url.includes(isMock ? value.mock : value.real) 16 | }) 17 | } 18 | 19 | api.interceptors.request.use( 20 | (config) => { 21 | const authStore = useAuthStore() 22 | const loaderStore = useLoaderStore() 23 | const settingsStore = useSettingsStore() 24 | 25 | const { school: city } = settingsStore.settings 26 | const { token } = authStore 27 | 28 | const endpoint = findEndpoint(config.url) 29 | const id = nanoid() 30 | 31 | config.headers.Authorization = `Bearer ${token}` 32 | config.params = { ...config.params, city } 33 | config.id = id 34 | 35 | loaderStore.loadingQueue.push({ key: endpoint, id }) 36 | 37 | return config 38 | }, 39 | (error) => { 40 | console.log(`API Call error: ${error}`) 41 | return Promise.reject(error) 42 | } 43 | ) 44 | 45 | api.interceptors.response.use( 46 | (response) => { 47 | const id = response.config.id 48 | 49 | const loaderStore = useLoaderStore() 50 | loaderStore.loadingQueue = loaderStore.loadingQueue.filter((item) => { 51 | return item.id !== id 52 | }) 53 | 54 | return response.data 55 | }, 56 | (error) => { 57 | if (!error.response) return Promise.reject(error) 58 | 59 | error.response.data.message ?? 60 | (error.response.data.message = DEFAULT_ERROR_MESSAGE) 61 | 62 | const id = error.response.config.id 63 | const endpoint = findEndpoint(error.response.config.url) 64 | 65 | const loaderStore = useLoaderStore() 66 | loaderStore.loadingQueue = loaderStore.loadingQueue.filter((item) => { 67 | return item.id !== id 68 | }) 69 | loaderStore.errors.push({ 70 | key: endpoint, 71 | message: error.response.data.message, 72 | }) 73 | 74 | return Promise.reject(error) 75 | } 76 | ) 77 | 78 | const createEndpoint = (name) => { 79 | const item = ENDPOINTS[name].endpoint 80 | return isMock ? item.mock : item.real 81 | } 82 | 83 | export const checkHealth = () => { 84 | return api.get(createEndpoint("HEALTH_SMS")) 85 | } 86 | 87 | export const getCity = () => { 88 | return api.get(createEndpoint("CITY"), { timeout: 1500 }) 89 | } 90 | 91 | export const login = (credentials = {}) => { 92 | return api.post(createEndpoint("LOGIN"), credentials) 93 | } 94 | 95 | export const refreshCaptcha = () => { 96 | return api.get(createEndpoint("REFRESH_CAPTCHA")) 97 | } 98 | 99 | export const getYears = () => { 100 | return api.get(createEndpoint("YEARS")) 101 | } 102 | 103 | export const getTerms = (yearId) => { 104 | return api.get(createEndpoint("TERMS") + yearId) 105 | } 106 | 107 | export const getDiary = (termId) => { 108 | return api.get(createEndpoint("DIARY") + termId) 109 | } 110 | 111 | export const getSubject = (journalId, evaluations) => { 112 | return api.get(createEndpoint("SUBJECT"), { 113 | params: { journalId, evaluations }, 114 | }) 115 | } 116 | 117 | export const getGrades = (yearID) => { 118 | return api.get(createEndpoint("GRADES"), { params: { yearID } }) 119 | } 120 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/notifications/Notification.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 101 | 102 | 134 | -------------------------------------------------------------------------------- /apps/web-mars/public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/api/src/routes/dashboard/grades/index.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch" 2 | import { FAKE_USER_AGENT } from "../../../config/index.js" 3 | 4 | export default async function (fastify) { 5 | fastify.get( 6 | "", 7 | { 8 | schema: { 9 | headers: fastify.getSchema("token"), 10 | querystring: { 11 | type: "object", 12 | required: ["city", "yearID"], 13 | properties: { 14 | city: fastify.getSchema("city"), 15 | yearID: { type: "string" }, 16 | }, 17 | }, 18 | response: { 19 | 200: { 20 | type: "array", 21 | items: { 22 | type: "object", 23 | properties: { 24 | SubjectName: { type: "string" }, 25 | FirstPeriod: { type: "string" }, 26 | SecondPeriod: { type: "string" }, 27 | FirstHalfYear: { type: "string" }, 28 | ThirdPeriod: { type: "string" }, 29 | ForthPeriod: { type: "string" }, 30 | SecondHalfYear: { type: "string" }, 31 | Exam: { type: "string" }, 32 | Year: { type: "string" }, 33 | Final: { type: "string" }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | tags: ["dashboard"], 39 | }, 40 | }, 41 | async (req, reply) => { 42 | const { city, yearID } = req.query 43 | const baseUrl = `https://sms.${city}.nis.edu.kz` 44 | 45 | const params = new URLSearchParams() 46 | const cookie = req.cookies 47 | 48 | const organization = await fastify.api({ 49 | method: "POST", 50 | cookie, 51 | url: `${baseUrl}/reportcard/GetOrganizations`, 52 | }) 53 | 54 | params.append("schoolYearId", yearID) 55 | params.append("organizationId", organization.data[0].Id) 56 | params.append("organizationInternalId", organization.data[0].Id) 57 | 58 | const parallels = await fastify.api({ 59 | method: "POST", 60 | body: params, 61 | cookie, 62 | url: `${baseUrl}/reportcard/GetParallels`, 63 | }) 64 | 65 | params.append("parallelId", parallels.data[0].Id) 66 | 67 | const klasses = await fastify.api({ 68 | method: "POST", 69 | body: params, 70 | cookie, 71 | url: `${baseUrl}/reportcard/GetKlasses`, 72 | }) 73 | 74 | params.append("klassId", klasses.data[0].Id) 75 | 76 | const students = await fastify.api({ 77 | method: "POST", 78 | body: params, 79 | cookie, 80 | url: `${baseUrl}/reportcard/GetStudents`, 81 | }) 82 | 83 | params.append("personId", students.data[0].Id) 84 | params.append("isEditable", true) 85 | params.append("group", { property: "ComponentId", direction: "ASC" }) 86 | 87 | const { data: url, cookie: resCookie } = await fastify.api({ 88 | method: "POST", 89 | body: params, 90 | cookie, 91 | url: `${baseUrl}/reportcard/GetUrl`, 92 | }) 93 | 94 | const newCookies = fastify.mergeCookies(cookie, resCookie) 95 | 96 | await fetch(url, { 97 | headers: { 98 | cookie: newCookies, 99 | "user-agent": FAKE_USER_AGENT, 100 | }, 101 | }) 102 | 103 | const grades = await fastify.api({ 104 | method: "POST", 105 | body: params, 106 | cookie: newCookies, 107 | url: `${baseUrl}/ReportCardByStudent/GetData`, 108 | }) 109 | 110 | const array = grades.data.filter( 111 | (grade) => 112 | grade.IsNotChosen && grade.ComponentName === "Инвариантный компонент" 113 | ) 114 | 115 | // remove item duplicates 116 | await reply.send([ 117 | ...new Map(array.map((item) => [item["SubjectName"], item])).values(), 118 | ]) 119 | } 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/layout/subject/SubjectGrades.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 88 | 89 | 121 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/Button.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 117 | 118 | 159 | -------------------------------------------------------------------------------- /apps/api/src/routes/dashboard/diary/_termID/index.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch" 2 | import { URLSearchParams } from "url" 3 | 4 | export default async function (fastify) { 5 | fastify.get( 6 | "", 7 | { 8 | schema: { 9 | querystring: fastify.getSchema("domain"), 10 | headers: fastify.getSchema("token"), 11 | params: { 12 | type: "object", 13 | required: ["termID"], 14 | properties: { 15 | termID: { type: "string", minLength: 36, maxLength: 36 }, 16 | }, 17 | }, 18 | response: { 19 | 200: { 20 | type: "object", 21 | properties: { 22 | data: { 23 | type: "array", 24 | items: { 25 | type: "object", 26 | properties: { 27 | Name: { type: "string" }, 28 | JournalId: { type: "string" }, 29 | Score: { type: "number" }, 30 | Mark: { type: "number" }, 31 | Evaluations: { 32 | type: "array", 33 | items: { 34 | type: "string", 35 | }, 36 | }, 37 | }, 38 | }, 39 | }, 40 | token: { type: "string" }, 41 | }, 42 | }, 43 | }, 44 | tags: ["dashboard"], 45 | }, 46 | }, 47 | async (req, reply) => { 48 | const baseUrl = `https://sms.${req.query.city}.nis.edu.kz` 49 | 50 | let cookie = req.cookies 51 | 52 | const params = new URLSearchParams() 53 | params.append("periodId", req.params.termID) 54 | 55 | const parallel = await fastify.api({ 56 | url: `${baseUrl}/JceDiary/GetParallels`, 57 | method: "POST", 58 | body: params, 59 | cookie, 60 | }) 61 | 62 | params.append("parallelId", parallel.data[0].Id) 63 | 64 | const klasses = await fastify.api({ 65 | url: `${baseUrl}/JceDiary/GetKlasses`, 66 | method: "POST", 67 | body: params, 68 | cookie, 69 | }) 70 | 71 | const realKlass = 72 | klasses.data.length === 1 73 | ? klasses.data[0] 74 | : klasses.data.find((cur, id) => { 75 | if (id === 0) return false 76 | 77 | return klasses.data[id - 1].Id === cur.Id 78 | }) 79 | 80 | if (!realKlass) { 81 | const err = new Error("Класс ученика не найден") 82 | err.code = 404 83 | throw err 84 | } 85 | 86 | params.append("klassId", realKlass.Id) 87 | 88 | const student = await fastify.api({ 89 | url: `${baseUrl}/JceDiary/GetStudents`, 90 | method: "POST", 91 | body: params, 92 | cookie, 93 | }) 94 | 95 | params.append("studentId", student.data[0].Id) 96 | 97 | const diaryLink = await fastify.api({ 98 | url: `${baseUrl}/JceDiary/GetJceDiary`, 99 | method: "POST", 100 | body: params, 101 | cookie, 102 | }) 103 | 104 | const cookieResponse = await fetch(diaryLink.data.Url, { 105 | method: "POST", 106 | headers: { cookie }, 107 | body: params, 108 | }) 109 | 110 | const newCookies = fastify.cookieParse(cookieResponse) 111 | 112 | if (newCookies) cookie = fastify.mergeCookies(cookie, newCookies) 113 | 114 | const periodsData = await fastify.api({ 115 | url: `${baseUrl}/Jce/Diary/GetSubjects`, 116 | method: "POST", 117 | body: params, 118 | cookie, 119 | }) 120 | 121 | const signJWT = new Promise((resolve, reject) => { 122 | fastify.jwt.sign( 123 | { cookies: cookie, account: req.account }, 124 | null, 125 | (err, token) => { 126 | if (err) return reject(err) 127 | resolve(token) 128 | } 129 | ) 130 | }) 131 | 132 | const token = await signJWT 133 | 134 | await reply.send({ 135 | data: periodsData.data.map((el) => ({ 136 | ...el, 137 | Evaluations: el.Evaluations.map((el2) => el2.Id), 138 | })), 139 | token, 140 | }) 141 | } 142 | ) 143 | } 144 | -------------------------------------------------------------------------------- /apps/web-mars/src/components/base/app/Logo.vue: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /apps/web-mars/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 170 | -------------------------------------------------------------------------------- /apps/web-mars/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 121 | 122 | 310 | --------------------------------------------------------------------------------