├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── codeql.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── BaseClient.ts ├── Client.ts ├── WebClient.ts ├── _test.ts ├── index.ts ├── schemas │ ├── index.ts │ └── utilityTypes.ts ├── types │ ├── apiTypes.ts │ ├── general.ts │ └── index.ts ├── util │ ├── Constants.ts │ ├── encryptCodeVerifier.ts │ ├── formatDate.ts │ ├── generateLoginLink.ts │ ├── getCode.ts │ ├── getToken.ts │ ├── handleOperation.ts │ ├── importData.ts │ ├── index.ts │ ├── randomString.ts │ └── writeToFile.ts └── web.ts ├── tsconfig.json └── tsup.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const { builtinModules } = require("node:module"); 2 | 3 | module.exports = { 4 | ignorePatterns: ["dist/", "*.cjs", "_test.ts", "tsup.config.ts"], 5 | env: { 6 | node: true, 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 12 | "plugin:node/recommended", 13 | ], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | sourceType: "module", 17 | project: "tsconfig.json", 18 | tsconfigRootDir: ".", 19 | }, 20 | plugins: ["@typescript-eslint", "node"], 21 | rules: { 22 | "no-extend-native": "warn", 23 | "no-iterator": "warn", 24 | "no-lone-blocks": "warn", 25 | "no-return-assign": "warn", 26 | "no-useless-computed-key": "warn", 27 | curly: ["warn", "multi"], 28 | "dot-location": ["warn", "property"], 29 | eqeqeq: ["warn", "smart"], 30 | "no-else-return": [ 31 | "warn", 32 | { 33 | allowElseIf: false, 34 | }, 35 | ], 36 | "no-empty": "off", 37 | "no-extra-bind": "warn", 38 | "no-floating-decimal": "warn", 39 | "no-implicit-coercion": "warn", 40 | "no-multi-spaces": "warn", 41 | "no-restricted-imports": [ 42 | "warn", 43 | ...builtinModules.map((name) => ({ 44 | name, 45 | message: "Use the `node:` protocol to import a built-in module", 46 | })), 47 | ], 48 | "no-useless-return": "warn", 49 | "wrap-iife": ["warn", "inside"], 50 | yoda: [ 51 | "warn", 52 | "never", 53 | { 54 | exceptRange: true, 55 | }, 56 | ], 57 | "no-undef-init": "warn", 58 | "array-bracket-newline": ["warn", "consistent"], 59 | "array-element-newline": ["warn", "consistent"], 60 | "computed-property-spacing": "warn", 61 | "new-parens": "warn", 62 | "no-async-promise-executor": "off", 63 | "no-lonely-if": "warn", 64 | "no-mixed-spaces-and-tabs": "off", 65 | "no-multiple-empty-lines": "warn", 66 | "no-unneeded-ternary": [ 67 | "warn", 68 | { 69 | defaultAssignment: false, 70 | }, 71 | ], 72 | "no-whitespace-before-property": "warn", 73 | "one-var-declaration-per-line": "warn", 74 | "operator-assignment": "warn", 75 | "accessor-pairs": "warn", 76 | "array-callback-return": "warn", 77 | "arrow-body-style": "warn", 78 | "consistent-return": "warn", 79 | "default-case-last": "warn", 80 | "default-case": "warn", 81 | "grouped-accessor-pairs": "warn", 82 | "guard-for-in": "warn", 83 | "no-alert": "warn", 84 | "no-await-in-loop": "warn", 85 | "no-caller": "warn", 86 | "no-case-declarations": "off", 87 | "no-constructor-return": "warn", 88 | "no-labels": "warn", 89 | "no-multi-str": "warn", 90 | "no-new": "warn", 91 | "no-new-func": "warn", 92 | "no-new-wrappers": "warn", 93 | "no-octal-escape": "warn", 94 | "no-process-exit": "off", 95 | "no-promise-executor-return": "warn", 96 | "no-proto": "warn", 97 | "no-self-compare": "warn", 98 | "no-sequences": "warn", 99 | "no-template-curly-in-string": "warn", 100 | "no-unmodified-loop-condition": "warn", 101 | "no-unreachable-loop": "warn", 102 | "no-unsafe-optional-chaining": "warn", 103 | "no-unused-expressions": "warn", 104 | "no-useless-backreference": "warn", 105 | "no-useless-call": "warn", 106 | "no-useless-concat": "warn", 107 | "no-useless-rename": "warn", 108 | "no-var": "warn", 109 | "no-warning-comments": "warn", 110 | "node/no-missing-import": "off", 111 | "node/no-unsupported-features/es-syntax": [ 112 | "error", 113 | { 114 | ignores: ["dynamicImport", "modules"], 115 | }, 116 | ], 117 | "node/prefer-global/buffer": ["warn", "never"], 118 | "node/prefer-global/console": "warn", 119 | "node/prefer-global/text-decoder": "warn", 120 | "node/prefer-global/text-encoder": "warn", 121 | "node/prefer-global/url-search-params": "warn", 122 | "node/prefer-global/url": "warn", 123 | "node/prefer-promises/dns": "warn", 124 | "node/prefer-promises/fs": "warn", 125 | "object-shorthand": "warn", 126 | "prefer-arrow-callback": "warn", 127 | "prefer-const": "warn", 128 | "prefer-destructuring": "warn", 129 | "prefer-numeric-literals": "warn", 130 | "prefer-promise-reject-errors": "warn", 131 | "prefer-regex-literals": "warn", 132 | "prefer-rest-params": "warn", 133 | "prefer-spread": "warn", 134 | "prefer-template": "warn", 135 | "semi-spacing": "warn", 136 | "semi-style": "warn", 137 | "sort-vars": "warn", 138 | "symbol-description": "warn", 139 | "@typescript-eslint/array-type": "warn", 140 | "@typescript-eslint/consistent-type-assertions": "warn", 141 | "@typescript-eslint/consistent-type-definitions": ["warn", "type"], 142 | "@typescript-eslint/class-literal-property-style": "warn", 143 | "@typescript-eslint/member-ordering": "warn", 144 | "@typescript-eslint/no-confusing-non-null-assertion": "warn", 145 | "@typescript-eslint/no-confusing-void-expression": "warn", 146 | "@typescript-eslint/no-explicit-any": "off", 147 | "@typescript-eslint/no-extraneous-class": "warn", 148 | "@typescript-eslint/no-implied-eval": "off", 149 | "@typescript-eslint/no-loop-func": "warn", 150 | "@typescript-eslint/no-loss-of-precision": "warn", 151 | "@typescript-eslint/no-misused-promises": "off", 152 | "@typescript-eslint/no-non-null-assertion": "off", 153 | "@typescript-eslint/parameter-properties": "warn", 154 | "@typescript-eslint/no-require-imports": "off", 155 | "@typescript-eslint/no-shadow": "warn", 156 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn", 157 | "@typescript-eslint/no-unnecessary-condition": "warn", 158 | "@typescript-eslint/no-unnecessary-qualifier": "warn", 159 | "@typescript-eslint/no-unnecessary-type-constraint": "warn", 160 | "@typescript-eslint/no-unsafe-argument": "off", 161 | "@typescript-eslint/no-unsafe-assignment": "off", 162 | "@typescript-eslint/no-unused-vars": "off", 163 | "@typescript-eslint/no-use-before-define": "warn", 164 | "@typescript-eslint/no-useless-constructor": "warn", 165 | "@typescript-eslint/no-var-requires": "off", 166 | "@typescript-eslint/non-nullable-type-assertion-style": "warn", 167 | "@typescript-eslint/prefer-for-of": "warn", 168 | "@typescript-eslint/prefer-includes": "warn", 169 | "@typescript-eslint/prefer-nullish-coalescing": [ 170 | "warn", 171 | { ignoreConditionalTests: true }, 172 | ], 173 | "@typescript-eslint/prefer-optional-chain": "warn", 174 | "@typescript-eslint/prefer-string-starts-ends-with": "warn", 175 | "@typescript-eslint/prefer-ts-expect-error": "warn", 176 | "@typescript-eslint/require-await": "off", 177 | "@typescript-eslint/return-await": "warn", 178 | "@typescript-eslint/sort-type-constituents": "warn", 179 | "@typescript-eslint/switch-exhaustiveness-check": "warn", 180 | "@typescript-eslint/unified-signatures": "warn", 181 | }, 182 | }; 183 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: DTrombett 7 | 8 | --- 9 | 10 | ## Bug description 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## To Reproduce 15 | 16 | How to reproduce the bug. 17 | Provide a code sample, if possible. 18 | 19 | ## Expected behaviour 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Additional context 24 | 25 | Add any other context about the problem here. 26 | 27 | ## Version 28 | 29 | What version of the package are you using? 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: DTrombett 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | ## Describe the solution you'd like 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | ## Describe alternatives you've considered 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | ## Additional context 23 | 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "14:00" 8 | timezone: "Europe/Rome" 9 | commit-message: 10 | prefix: "deps" 11 | include: "scope" 12 | assignees: 13 | - "DTrombett" 14 | open-pull-requests-limit: 10 15 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '37 1 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v3 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v2 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v2 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v2 75 | with: 76 | category: "/language:${{matrix.language}}" 77 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Typescript, eslint and tsup tests 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - name: Tests with Node.js 20 9 | uses: actions/setup-node@v4 10 | with: 11 | node-version: 20.x 12 | cache: "npm" 13 | - run: npm ci --legacy-peer-deps 14 | - run: npm run test:cli 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | coverage/ 4 | dist/ 5 | .argo/ 6 | .vscode/settings.json 7 | ./types/ 8 | tsconfig.tsbuildinfo 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Start project", 6 | "command": "npm run start", 7 | "request": "launch", 8 | "type": "node-terminal", 9 | "autoAttachChildProcesses": true, 10 | "envFile": "${workspaceFolder}/.env", 11 | "cwd": "${workspaceFolder}", 12 | "sourceMaps": true 13 | }, 14 | ] 15 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | maxtrombdt@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You can contribute to this project by forking the repository, cloning it locally, making your fantastic changes and submitting a pull request. 4 | 5 | Use these steps to contribute: 6 | 7 | 1. Fork the repository 8 | 2. Clone the repository locally 9 | 3. Run `npm ci` to install all the dependencies 10 | 4. Make your changes 11 | 5. Run `npm lint` and `npm test` to make sure everything is working 12 | 6. Commit and push the changes to your fork 13 | 7. Submit a pull request! 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 D Trombett 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PortaleArgo API 2 | 3 | `portaleargo-api` è un modulo Node.js che fornisce un'interfaccia per interagire con il registro elettronico Argo. 4 | 5 | ## Installazione 6 | 7 | Per installare il pacchetto, eseguire il comando: 8 | 9 | ```sh 10 | npm install portaleargo-api 11 | ``` 12 | 13 | **Nota: Node.js v18 è raccomandato. Non garantiamo stabilità per le versioni inferiori.** 14 | 15 | **Nota: Al momento non abbiamo ancora pubblicato il progetto su npm. Se volete già provarlo potete installarlo tramite `npm i dtrombett/portaleargo-api`** 16 | 17 | ## Utilizzo 18 | 19 | Per utilizzare il pacchetto, importare la classe `Client` e istanziarla passando i seguenti parametri: 20 | 21 | - `schoolCode`: il codice della scuola. Se non fornito, verrà utilizzato il valore della variabile d'ambiente `CODICE_SCUOLA`. 22 | - `username`: il nome utente per accedere al registro elettronico. Se non fornito, verrà utilizzato il valore della variabile d'ambiente `NOME_UTENTE`. 23 | - `password`: la password per accedere al registro elettronico. Se non fornito, verrà utilizzato il valore della variabile d'ambiente `PASSWORD`. 24 | 25 | Esempio di utilizzo: 26 | 27 | ```js 28 | import { Client } from "portaleargo-api"; 29 | 30 | const client = new Client({ 31 | schoolCode: "SS13325", 32 | username: "dtrombett", 33 | password: "password123", 34 | }); 35 | 36 | // Effettua il login 37 | await client.login(); 38 | // `client.dashboard` contiene la maggior parte dei dati di cui hai bisogno 39 | console.log(client.dashboard.voti); 40 | // Per altri dati potrebbe essere necessaria una nuova richiesta 41 | const dettagliProfilo = await client.getDettagliProfilo(); 42 | 43 | console.log(dettagliProfilo.genitore.email); 44 | ``` 45 | 46 | ## TypeScript 47 | 48 | La libreria è scritta interamente in TypeScript, quindi contiene supporti per i tipi. 49 | 50 | Alcuni tipi di campi restituiti dall'API contengono `any` in quanto nei nostri test contenevano dati incompleti o mancanti perciò non possiamo stabilire con certezza quale sia il loro tipo. 51 | Se notate che nel vostro profilo tali dati sono invece presenti vi invitiamo gentilmente ad aprire un issue o una pull request. 52 | 53 | ## Come contribuire 54 | 55 | A seguito dei nostri test, abbiamo aggiunto il supporto per tutti gli endpoint che siamo riusciti a trovare. 56 | Se pensi di aver trovato un altro endpoint, o alcuni dati visibili nell'app non sono presenti tramite alcun metodo, sentiti libero di aprire un issue su GitHub. 57 | 58 | Puoi aprire un issue anche se pensi di aver trovato un bug nella libreria o hai qualsiasi dubbio, domanda o suggerimento. 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portaleargo-api", 3 | "version": "1.0.0", 4 | "description": "A library to interact with the api of ArgoScuolaNext", 5 | "exports": { 6 | "./web": "./dist/web.js", 7 | ".": "./dist/index.js" 8 | }, 9 | "type": "module", 10 | "main": "./dist/index.js", 11 | "types": "./dist/index.d.ts", 12 | "scripts": { 13 | "build": "tsup && tsc", 14 | "lint": "eslint src --fix", 15 | "prettier": "prettier --write src/**/*.ts", 16 | "test": "npm run test:cli && cross-env NODE_ENV=development tsup && node --enable-source-maps dist/_test.js", 17 | "test:cli": "eslint src && tsc --noEmit", 18 | "postinstall": "npm run build" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/DTrombett/portaleargo-api.git" 23 | }, 24 | "author": "DTrombett", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/DTrombett/portaleargo-api/issues" 28 | }, 29 | "homepage": "https://github.com/DTrombett/portaleargo-api#readme", 30 | "engines": { 31 | "node": ">=18" 32 | }, 33 | "dependencies": { 34 | "http-cookie-agent": "^6.0.8", 35 | "tough-cookie": "^5.1.2", 36 | "undici": "^7.5.0" 37 | }, 38 | "devDependencies": { 39 | "@tsconfig/recommended": "^1.0.8", 40 | "@tsconfig/strictest": "^2.0.5", 41 | "@types/node": "^18.19.80", 42 | "@typescript-eslint/eslint-plugin": "^8.26.1", 43 | "@typescript-eslint/parser": "^8.26.1", 44 | "cross-env": "^7.0.3", 45 | "dotenv": "^16.4.7", 46 | "eslint": "^8.57.1", 47 | "eslint-plugin-node": "^11.1.0", 48 | "prettier": "^3.5.3", 49 | "tsup": "^8.4.0", 50 | "typescript": "^5.8.2" 51 | }, 52 | "optionalDependencies": { 53 | "ajv": "^8.17.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/BaseClient.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIBacheca, 3 | APIBachecaAlunno, 4 | APICorsiRecupero, 5 | APICurriculum, 6 | APIDashboard, 7 | APIDettagliProfilo, 8 | APIDownloadAllegato, 9 | APILogin, 10 | APIOrarioGiornaliero, 11 | APIPCTO, 12 | APIProfilo, 13 | APIResponse, 14 | APIRicevimenti, 15 | APIRicevutaTelematica, 16 | APITasse, 17 | APIToken, 18 | APIVotiScrutinio, 19 | APIWhat, 20 | ClientOptions, 21 | Credentials, 22 | Dashboard, 23 | HttpMethod, 24 | Json, 25 | LoginLink, 26 | ReadyClient, 27 | Token, 28 | } from "./types"; 29 | import { 30 | clientId, 31 | defaultVersion, 32 | formatDate, 33 | getToken, 34 | handleOperation, 35 | randomString, 36 | } from "./util"; 37 | 38 | /** 39 | * Un client per interagire con l'API 40 | */ 41 | export abstract class BaseClient { 42 | static readonly BASE_URL = "https://www.portaleargo.it"; 43 | 44 | /** 45 | * A custom fetch implementation 46 | */ 47 | fetch = fetch; 48 | 49 | /** 50 | * I dati del token 51 | */ 52 | token?: Token; 53 | 54 | /** 55 | * I dati del login 56 | */ 57 | loginData?: APILogin["data"][number]; 58 | 59 | /** 60 | * I dati del profilo 61 | */ 62 | profile?: APIProfilo["data"]; 63 | 64 | /** 65 | * I dati della dashboard 66 | */ 67 | dashboard?: Dashboard; 68 | 69 | /** 70 | * Se scrivere nella console alcuni dati utili per il debug 71 | */ 72 | debug: boolean; 73 | 74 | /** 75 | * Headers aggiuntivi per ogni richiesta API 76 | */ 77 | headers?: Record; 78 | 79 | /** 80 | * Le funzioni per leggere e scrivere i dati. 81 | * Impostare questo valore forzerà `dataPath` a `null` 82 | */ 83 | dataProvider?: NonNullable; 84 | 85 | /** 86 | * La versione di didUp da specificare nell'header. 87 | * * Modificare questa opzione potrebbe creare problemi nell'utilizzo della libreria 88 | */ 89 | version: string; 90 | 91 | /** 92 | * Le credenziali usate per l'accesso 93 | */ 94 | credentials?: Partial; 95 | 96 | #ready = false; 97 | 98 | /** 99 | * @param options - Le opzioni per il client 100 | */ 101 | constructor(options: ClientOptions = {}) { 102 | this.credentials = { 103 | schoolCode: options.schoolCode, 104 | password: options.password, 105 | username: options.username, 106 | }; 107 | this.token = options.token; 108 | this.loginData = options.loginData; 109 | this.profile = options.profile; 110 | this.dashboard = options.dashboard; 111 | this.debug = options.debug ?? false; 112 | this.version = options.version ?? defaultVersion; 113 | this.headers = options.headers; 114 | if (options.dataProvider !== null) this.dataProvider = options.dataProvider; 115 | } 116 | 117 | /** 118 | * Controlla se il client è pronto 119 | */ 120 | isReady(): this is ReadyClient { 121 | return this.#ready; 122 | } 123 | 124 | /** 125 | * Effettua una richiesta API. 126 | * @param path - Il percorso della richiesta 127 | * @param options - Altre opzioni 128 | * @returns La risposta 129 | */ 130 | apiRequest( 131 | path: string, 132 | options?: Partial<{ 133 | body: Json; 134 | method: HttpMethod; 135 | noWait: false; 136 | }>, 137 | ): Promise; 138 | apiRequest( 139 | path: string, 140 | options: { 141 | body?: Json; 142 | method?: HttpMethod; 143 | noWait: true; 144 | }, 145 | ): Promise & { json: () => Promise }>; 146 | async apiRequest( 147 | path: string, 148 | options: Partial<{ 149 | body: Json; 150 | method: HttpMethod; 151 | noWait: boolean; 152 | }> = {}, 153 | ): Promise { 154 | const headers: Record = { 155 | accept: "application/json", 156 | "argo-client-version": this.version, 157 | authorization: `Bearer ${this.token?.access_token ?? ""}`, 158 | }; 159 | 160 | options.method ??= options.body ? "POST" : "GET"; 161 | if (options.body != null) headers["content-type"] = "application/json"; 162 | if (this.loginData) { 163 | headers["x-auth-token"] = this.loginData.token; 164 | headers["x-cod-min"] = this.loginData.codMin; 165 | } 166 | if (this.token) 167 | headers["x-date-exp-auth"] = formatDate(this.token.expireDate); 168 | if (this.headers) Object.assign(headers, this.headers); 169 | const res = await this.fetch( 170 | `${BaseClient.BASE_URL}/appfamiglia/api/rest/${path}`, 171 | { 172 | headers, 173 | method: options.method, 174 | body: options.body != null ? JSON.stringify(options.body) : undefined, 175 | }, 176 | ); 177 | 178 | if (this.debug) console.debug(`${options.method} /${path} ${res.status}`); 179 | return options.noWait ? res : (res.json() as unknown); 180 | } 181 | 182 | /** 183 | * Effettua il login. 184 | * @returns Il client aggiornato 185 | */ 186 | async login() { 187 | await Promise.all([ 188 | this.token && this.dataProvider?.write("token", this.token), 189 | this.loginData && this.dataProvider?.write("login", this.loginData), 190 | this.profile && this.dataProvider?.write("profile", this.profile), 191 | this.dashboard && this.dataProvider?.write("dashboard", this.dashboard), 192 | ]); 193 | await this.loadData(); 194 | const oldToken = this.token; 195 | 196 | await this.refreshToken(); 197 | if (!this.loginData) await this.getLoginData(); 198 | if (oldToken) { 199 | this.logToken({ 200 | oldToken, 201 | isWhat: this.profile !== undefined, 202 | }).catch(console.error); 203 | if (this.profile) { 204 | const whatData = await this.what( 205 | this.dashboard?.dataAggiornamento ?? this.profile.anno.dataInizio, 206 | ); 207 | 208 | if (whatData.isModificato || whatData.differenzaSchede) { 209 | Object.assign(this.profile, whatData); 210 | void this.dataProvider?.write("profile", this.profile); 211 | } 212 | this.#ready = true; 213 | if (whatData.mostraPallino || !this.dashboard) 214 | await this.getDashboard(); 215 | this.aggiornaData().catch(console.error); 216 | return this as ReadyClient & this & { dashboard: Dashboard }; 217 | } 218 | } 219 | if (!this.profile) await this.getProfilo(); 220 | this.#ready = true; 221 | await this.getDashboard(); 222 | return this as ReadyClient & this & { dashboard: Dashboard }; 223 | } 224 | 225 | /** 226 | * Carica i dati salvati localmente. 227 | */ 228 | async loadData() { 229 | if (!this.dataProvider?.read) return; 230 | const [token, loginData, profile, dashboard] = await Promise.all([ 231 | this.token ? undefined : this.dataProvider.read("token"), 232 | this.loginData ? undefined : this.dataProvider.read("login"), 233 | this.profile ? undefined : this.dataProvider.read("profile"), 234 | this.dashboard ? undefined : this.dataProvider.read("dashboard"), 235 | ]); 236 | 237 | if (token) 238 | this.token = { ...token, expireDate: new Date(token.expireDate) }; 239 | if (loginData) this.loginData = loginData; 240 | if (profile) this.profile = profile; 241 | if (dashboard) 242 | this.dashboard = { 243 | ...dashboard, 244 | dataAggiornamento: new Date(dashboard.dataAggiornamento), 245 | }; 246 | } 247 | 248 | /** 249 | * Aggiorna il client, se necessario. 250 | * @returns Il nuovo token 251 | */ 252 | async refreshToken() { 253 | if (!this.token) return this.getToken(); 254 | if (this.token.expireDate.getTime() <= Date.now()) { 255 | const date = new Date(); 256 | const res = await this.apiRequest("auth/refresh-token", { 257 | body: { 258 | "r-token": this.token.refresh_token, 259 | "client-id": clientId, 260 | scopes: `[${this.token.scope.split(" ").join(", ")}]`, 261 | "old-bearer": this.token.access_token, 262 | "primo-accesso": "false", 263 | "ripeti-login": "false", 264 | "exp-bearer": formatDate(this.token.expireDate), 265 | "ts-app": formatDate(date), 266 | proc: "initState_global_random_12345", 267 | username: this.loginData?.username, 268 | }, 269 | noWait: true, 270 | }); 271 | const expireDate = new Date(res.headers.get("date") ?? date); 272 | const token = await res.json(); 273 | 274 | if ("error" in token) 275 | throw new Error(`${token.error} ${token.error_description}`); 276 | expireDate.setSeconds(expireDate.getSeconds() + token.expires_in); 277 | this.token = Object.assign(this.token, token, { expireDate }); 278 | void this.dataProvider?.write("token", this.token); 279 | } 280 | return this.token; 281 | } 282 | 283 | /** 284 | * Ottieni il token tramite l'API. 285 | * @param code - The code for the access 286 | * @returns I dati del token 287 | */ 288 | async getToken(code?: LoginLink & { code: string }) { 289 | code ??= await this.getCode(); 290 | const { expireDate, ...token } = await getToken(code); 291 | 292 | this.token = Object.assign(this.token ?? {}, token, { expireDate }); 293 | void this.dataProvider?.write("token", this.token); 294 | return this.token; 295 | } 296 | 297 | /** 298 | * Rimuovi il profilo. 299 | */ 300 | async logOut() { 301 | if (!this.token || !this.loginData) 302 | throw new Error("Client is not logged in!"); 303 | await this.rimuoviProfilo(); 304 | delete this.token; 305 | delete this.loginData; 306 | delete this.profile; 307 | delete this.dashboard; 308 | } 309 | 310 | /** 311 | * Ottieni i dettagli del profilo dello studente. 312 | * @returns I dati 313 | */ 314 | async getDettagliProfilo(old?: T) { 315 | this.checkReady(); 316 | const body = await this.apiRequest("dettaglioprofilo", { 317 | method: "POST", 318 | }); 319 | 320 | if (!body.success) throw new Error(body.msg!); 321 | return Object.assign(old ?? {}, body.data); 322 | } 323 | 324 | /** 325 | * Ottieni l'orario giornaliero. 326 | * @param date - Il giorno dell'orario 327 | * @returns I dati 328 | */ 329 | async getOrarioGiornaliero(date?: { 330 | year?: number; 331 | month?: number; 332 | day?: number; 333 | }) { 334 | this.checkReady(); 335 | const now = new Date(); 336 | const orario = await this.apiRequest( 337 | "orario-giorno", 338 | { 339 | body: { 340 | datGiorno: formatDate( 341 | `${date?.year ?? now.getFullYear()}-${ 342 | date?.month ?? now.getMonth() + 1 343 | }-${date?.day ?? now.getDate() + 1}`, 344 | ), 345 | }, 346 | }, 347 | ); 348 | 349 | if (!orario.success) throw new Error(orario.msg!); 350 | return Object.values(orario.data.dati).flat(); 351 | } 352 | 353 | /** 354 | * Ottieni il link per scaricare un allegato della bacheca. 355 | * @param uid - L'uid dell'allegato 356 | * @returns L'url 357 | */ 358 | async getLinkAllegato(uid: string) { 359 | this.checkReady(); 360 | const download = await this.apiRequest( 361 | "downloadallegatobacheca", 362 | { body: { uid } }, 363 | ); 364 | 365 | if (!download.success) throw new Error(download.msg); 366 | return download.url; 367 | } 368 | 369 | /** 370 | * Ottieni il link per scaricare un allegato della bacheca alunno. 371 | * @param uid - l'uid dell'allegato 372 | * @param pkScheda - L'id del profilo 373 | * @returns L'url 374 | */ 375 | async getLinkAllegatoStudente( 376 | uid: string, 377 | pkScheda = this.profile?.scheda.pk, 378 | ) { 379 | this.checkReady(); 380 | const download = await this.apiRequest( 381 | "downloadallegatobachecaalunno", 382 | { body: { uid, pkScheda } }, 383 | ); 384 | 385 | if (!download.success) throw new Error(download.msg); 386 | return download.url; 387 | } 388 | 389 | /** 390 | * Ottieni i dati di una ricevuta telematica. 391 | * @param iuv - L'iuv del pagamento 392 | * @returns La ricevuta 393 | */ 394 | async getRicevuta(iuv: string) { 395 | this.checkReady(); 396 | const ricevuta = await this.apiRequest( 397 | "ricevutatelematica", 398 | { body: { iuv } }, 399 | ); 400 | 401 | if (!ricevuta.success) throw new Error(ricevuta.msg); 402 | const { success, msg, ...rest } = ricevuta; 403 | 404 | return rest; 405 | } 406 | 407 | /** 408 | * Ottieni i voti dello scrutinio dello studente. 409 | * @returns I dati 410 | */ 411 | async getVotiScrutinio() { 412 | this.checkReady(); 413 | const voti = await this.apiRequest("votiscrutinio", { 414 | body: {}, 415 | }); 416 | 417 | if (!voti.success) throw new Error(voti.msg!); 418 | return voti.data.votiScrutinio[0]?.periodi; 419 | } 420 | 421 | /** 422 | * Ottieni i dati riguardo i ricevimenti dello studente. 423 | * @returns I dati 424 | */ 425 | async getRicevimenti(old?: T) { 426 | this.checkReady(); 427 | const ricevimenti = await this.apiRequest("ricevimento", { 428 | body: {}, 429 | }); 430 | 431 | if (!ricevimenti.success) throw new Error(ricevimenti.msg!); 432 | return Object.assign(old ?? {}, ricevimenti.data); 433 | } 434 | 435 | /** 436 | * Ottieni le tasse dello studente. 437 | * @param pkScheda - L'id del profilo 438 | * @returns I dati 439 | */ 440 | async getTasse(pkScheda = this.profile?.scheda.pk) { 441 | this.checkReady(); 442 | const taxes = await this.apiRequest("listatassealunni", { 443 | body: { pkScheda }, 444 | }); 445 | 446 | if (!taxes.success) throw new Error(taxes.msg!); 447 | const { success, msg, data, ...rest } = taxes; 448 | 449 | return { 450 | ...rest, 451 | tasse: data, 452 | }; 453 | } 454 | 455 | /** 456 | * Ottieni i dati del PCTO dello studente. 457 | * @param pkScheda - L'id del profilo 458 | * @returns I dati 459 | */ 460 | async getPCTOData(pkScheda = this.profile?.scheda.pk) { 461 | this.checkReady(); 462 | const pcto = await this.apiRequest("pcto", { 463 | body: { pkScheda }, 464 | }); 465 | 466 | if (!pcto.success) throw new Error(pcto.msg!); 467 | return pcto.data.pcto; 468 | } 469 | 470 | /** 471 | * Ottieni i dati dei corsi di recupero dello studente. 472 | * @param pkScheda - L'id del profilo 473 | * @returns I dati 474 | */ 475 | async getCorsiRecupero( 476 | pkScheda = this.profile?.scheda.pk, 477 | old?: T, 478 | ) { 479 | this.checkReady(); 480 | const courses = await this.apiRequest("corsirecupero", { 481 | body: { pkScheda }, 482 | }); 483 | 484 | if (!courses.success) throw new Error(courses.msg!); 485 | return Object.assign(old ?? {}, courses.data); 486 | } 487 | 488 | /** 489 | * Ottieni il curriculum dello studente. 490 | * @param pkScheda - L'id del profilo 491 | * @returns I dati 492 | */ 493 | async getCurriculum(pkScheda = this.profile?.scheda.pk) { 494 | this.checkReady(); 495 | const curriculum = await this.apiRequest( 496 | "curriculumalunno", 497 | { 498 | body: { pkScheda }, 499 | }, 500 | ); 501 | 502 | if (!curriculum.success) throw new Error(curriculum.msg!); 503 | return curriculum.data.curriculum; 504 | } 505 | 506 | /** 507 | * Ottieni lo storico della bacheca. 508 | * @param pkScheda - L'id del profilo 509 | * @returns I dati 510 | */ 511 | async getStoricoBacheca(pkScheda: string) { 512 | this.checkReady(); 513 | const bacheca = await this.apiRequest("storicobacheca", { 514 | body: { pkScheda }, 515 | }); 516 | 517 | if (!bacheca.success) throw new Error(bacheca.msg!); 518 | return handleOperation(bacheca.data.bacheca); 519 | } 520 | 521 | /** 522 | * Ottieni lo storico della bacheca alunno. 523 | * @param pkScheda - L'id del profilo 524 | * @returns I dati 525 | */ 526 | async getStoricoBachecaAlunno(pkScheda: string) { 527 | this.checkReady(); 528 | const bacheca = await this.apiRequest( 529 | "storicobachecaalunno", 530 | { 531 | body: { pkScheda }, 532 | }, 533 | ); 534 | 535 | if (!bacheca.success) throw new Error(bacheca.msg!); 536 | return handleOperation(bacheca.data.bachecaAlunno); 537 | } 538 | 539 | /** 540 | * Ottieni i dati della dashboard. 541 | * @returns La dashboard 542 | */ 543 | private async getDashboard() { 544 | this.checkReady(); 545 | const date = new Date(); 546 | const res = await this.apiRequest("dashboard/dashboard", { 547 | body: { 548 | dataultimoaggiornamento: formatDate( 549 | this.dashboard?.dataAggiornamento ?? this.profile.anno.dataInizio, 550 | ), 551 | opzioni: JSON.stringify( 552 | Object.fromEntries( 553 | (this.dashboard ?? this.loginData).opzioni.map((a) => [ 554 | a.chiave, 555 | a.valore, 556 | ]), 557 | ), 558 | ), 559 | }, 560 | noWait: true, 561 | }); 562 | const body = await res.json(); 563 | 564 | if (!body.success) throw new Error(body.msg!); 565 | const [data] = body.data.dati; 566 | 567 | this.dashboard = Object.assign( 568 | (data.rimuoviDatiLocali ? null : this.dashboard) ?? {}, 569 | { 570 | ...data, 571 | fuoriClasse: handleOperation( 572 | data.fuoriClasse, 573 | data.rimuoviDatiLocali ? undefined : this.dashboard?.fuoriClasse, 574 | ), 575 | promemoria: handleOperation( 576 | data.promemoria, 577 | data.rimuoviDatiLocali ? undefined : this.dashboard?.promemoria, 578 | ), 579 | bacheca: handleOperation( 580 | data.bacheca, 581 | data.rimuoviDatiLocali ? undefined : this.dashboard?.bacheca, 582 | ), 583 | voti: handleOperation( 584 | data.voti, 585 | data.rimuoviDatiLocali ? undefined : this.dashboard?.voti, 586 | ), 587 | bachecaAlunno: handleOperation( 588 | data.bachecaAlunno, 589 | data.rimuoviDatiLocali ? undefined : this.dashboard?.bachecaAlunno, 590 | ), 591 | registro: handleOperation( 592 | data.registro, 593 | data.rimuoviDatiLocali ? undefined : this.dashboard?.registro, 594 | ), 595 | appello: handleOperation( 596 | data.appello, 597 | data.rimuoviDatiLocali ? undefined : this.dashboard?.appello, 598 | ), 599 | prenotazioniAlunni: handleOperation( 600 | data.prenotazioniAlunni, 601 | data.rimuoviDatiLocali 602 | ? undefined 603 | : this.dashboard?.prenotazioniAlunni, 604 | (a) => a.prenotazione.pk, 605 | ), 606 | dataAggiornamento: new Date(res.headers.get("date") ?? date), 607 | }, 608 | ); 609 | void this.dataProvider?.write("dashboard", this.dashboard); 610 | return this.dashboard; 611 | } 612 | 613 | private async getProfilo() { 614 | const profile = await this.apiRequest("profilo"); 615 | 616 | if (!profile.success) throw new Error(profile.msg!); 617 | this.profile = Object.assign(this.profile ?? {}, profile.data); 618 | void this.dataProvider?.write("profile", this.profile); 619 | return this.profile; 620 | } 621 | 622 | private async getLoginData() { 623 | const login = await this.apiRequest("login", { 624 | body: { 625 | "lista-opzioni-notifiche": "{}", 626 | "lista-x-auth-token": "[]", 627 | clientID: randomString(163), 628 | }, 629 | }); 630 | 631 | if (!login.success) throw new Error(login.msg!); 632 | this.loginData = Object.assign(this.loginData ?? {}, login.data[0]); 633 | void this.dataProvider?.write("login", this.loginData); 634 | return this.loginData; 635 | } 636 | 637 | private async logToken(options: { oldToken: Token; isWhat?: boolean }) { 638 | const res = await this.apiRequest("logtoken", { 639 | body: { 640 | bearerOld: options.oldToken.access_token, 641 | dateExpOld: formatDate(options.oldToken.expireDate), 642 | refreshOld: options.oldToken.refresh_token, 643 | bearerNew: this.token?.access_token, 644 | dateExpNew: this.token?.expireDate && formatDate(this.token.expireDate), 645 | refreshNew: this.token?.refresh_token, 646 | isWhat: (options.isWhat ?? false).toString(), 647 | isRefreshed: ( 648 | this.token?.access_token === options.oldToken.access_token 649 | ).toString(), 650 | proc: "initState_global_random_12345", 651 | }, 652 | }); 653 | 654 | if (!res.success) throw new Error(res.msg!); 655 | } 656 | 657 | private async rimuoviProfilo() { 658 | const res = await this.apiRequest("rimuoviprofilo", { 659 | body: {}, 660 | }); 661 | 662 | if (!res.success) throw new Error(res.msg!); 663 | await this.dataProvider?.reset(); 664 | } 665 | 666 | private async what( 667 | lastUpdate: Date | number | string, 668 | old?: APIWhat["data"]["dati"][number], 669 | ) { 670 | const authToken = JSON.stringify([this.loginData?.token]); 671 | const opzioni = (this.dashboard ?? this.loginData)?.opzioni; 672 | const what = await this.apiRequest("dashboard/what", { 673 | body: { 674 | dataultimoaggiornamento: formatDate(lastUpdate), 675 | opzioni: 676 | opzioni && 677 | JSON.stringify( 678 | Object.fromEntries(opzioni.map((a) => [a.chiave, a.valore])), 679 | ), 680 | "lista-x-auth-token": authToken, 681 | "lista-x-auth-token-account": authToken, 682 | }, 683 | }); 684 | 685 | if (!what.success) throw new Error(what.msg!); 686 | return Object.assign(old ?? {}, what.data.dati[0]); 687 | } 688 | 689 | private async aggiornaData() { 690 | const res = await this.apiRequest("dashboard/aggiornadata", { 691 | body: { dataultimoaggiornamento: formatDate(new Date()) }, 692 | }); 693 | 694 | if (!res.success) throw new Error(res.msg!); 695 | } 696 | 697 | private checkReady(): asserts this is ReadyClient { 698 | if (!this.isReady()) throw new Error("Client is not logged in!"); 699 | } 700 | 701 | protected abstract getCode(): Promise; 702 | } 703 | -------------------------------------------------------------------------------- /src/Client.ts: -------------------------------------------------------------------------------- 1 | import { CookieClient } from "http-cookie-agent/undici"; 2 | import { existsSync } from "node:fs"; 3 | import { mkdir, rm } from "node:fs/promises"; 4 | import { join } from "node:path"; 5 | import { cwd, env } from "node:process"; 6 | import { CookieJar } from "tough-cookie"; 7 | import { 8 | fetch, 9 | interceptors, 10 | Pool, 11 | type Dispatcher, 12 | type RequestInfo, 13 | type RequestInit, 14 | type RetryHandler, 15 | } from "undici"; 16 | import type CacheHandler from "undici/types/cache-interceptor"; 17 | import { BaseClient } from "./BaseClient"; 18 | import type { ClientOptions, Credentials } from "./types"; 19 | import { getCode } from "./util/getCode"; 20 | import { importData } from "./util/importData"; 21 | import { writeToFile } from "./util/writeToFile"; 22 | 23 | const factory = (origin: import("url").URL, opts: object): CookieClient => 24 | new CookieClient(origin, { 25 | ...opts, 26 | cookies: { jar: new CookieJar() }, 27 | }); 28 | 29 | /** 30 | * Un client per interagire con l'API 31 | */ 32 | export class Client extends BaseClient { 33 | /** 34 | * Custom dispatcher. 35 | */ 36 | dispatcher: Dispatcher; 37 | 38 | override fetch = this.createFetch(); 39 | 40 | /** 41 | * @param options - Le opzioni per il client 42 | */ 43 | constructor( 44 | options: ClientOptions & { 45 | /** 46 | * Il percorso della cartella dove salvare i dati. 47 | * * Ignorato se `dataProvider` viene fornito 48 | */ 49 | dataPath?: string | null; 50 | 51 | /** 52 | * Additional options for the pool 53 | */ 54 | poolOptions?: Pool.Options; 55 | 56 | /** 57 | * Retry options 58 | */ 59 | retryOptions?: RetryHandler.RetryOptions; 60 | 61 | /** 62 | * Cache options 63 | */ 64 | cacheOptions?: CacheHandler.CacheOptions; 65 | } = {}, 66 | ) { 67 | super(options); 68 | this.credentials = { 69 | schoolCode: options.schoolCode ?? env.CODICE_SCUOLA, 70 | password: options.password ?? env.PASSWORD, 71 | username: options.username ?? env.NOME_UTENTE, 72 | }; 73 | this.dispatcher = new Pool(BaseClient.BASE_URL, { 74 | allowH2: true, 75 | autoSelectFamily: true, 76 | factory, 77 | ...options.poolOptions, 78 | }).compose( 79 | interceptors.retry({ 80 | maxRetries: 4, 81 | minTimeout: 100, 82 | timeoutFactor: 4, 83 | maxTimeout: 10_000, 84 | ...options.retryOptions, 85 | }), 86 | interceptors.cache({ 87 | cacheByDefault: 3_600_000, 88 | type: "private", 89 | ...options.cacheOptions, 90 | }), 91 | ); 92 | if (options.dataProvider !== null) 93 | this.dataProvider ??= Client.createDataProvider( 94 | options.dataPath ?? undefined, 95 | ); 96 | } 97 | 98 | static createDataProvider( 99 | dataPath = join(cwd(), ".argo"), 100 | ): NonNullable { 101 | let exists = existsSync(dataPath); 102 | 103 | return { 104 | read: (name) => importData(name, dataPath), 105 | write: async (name, value) => { 106 | if (!exists) { 107 | exists = true; 108 | await mkdir(dataPath); 109 | } 110 | return writeToFile(name, value, dataPath); 111 | }, 112 | reset: () => rm(dataPath, { recursive: true, force: true }), 113 | }; 114 | } 115 | 116 | createFetch(): typeof window.fetch { 117 | return (info, init) => 118 | fetch(info as RequestInfo, { 119 | dispatcher: this.dispatcher, 120 | ...(init as RequestInit), 121 | }) as unknown as Promise; 122 | } 123 | 124 | async getCode() { 125 | if ( 126 | [ 127 | this.credentials?.password, 128 | this.credentials?.schoolCode, 129 | this.credentials?.username, 130 | ].includes(undefined) 131 | ) 132 | throw new TypeError("Password, school code, or username missing"); 133 | return getCode(this.credentials as Credentials); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/WebClient.ts: -------------------------------------------------------------------------------- 1 | import { BaseClient } from "./BaseClient"; 2 | import type { ClientOptions } from "./types"; 3 | 4 | /** 5 | * Un client per interagire con l'API 6 | */ 7 | export class WebClient extends BaseClient { 8 | /** 9 | * @param options - Le opzioni per il client 10 | */ 11 | constructor(options: ClientOptions = {}) { 12 | super(options); 13 | if (options.dataProvider !== null) 14 | this.dataProvider ??= WebClient.createDataProvider(); 15 | } 16 | 17 | static createDataProvider(): NonNullable { 18 | return { 19 | read: async (name) => { 20 | const text = localStorage.getItem(name); 21 | 22 | if (text == null) return undefined; 23 | try { 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 25 | return JSON.parse(text); 26 | } catch (err) { 27 | return undefined; 28 | } 29 | }, 30 | write: async (name, value) => { 31 | try { 32 | const text = JSON.stringify(value); 33 | 34 | localStorage.setItem(name, text); 35 | } catch (err) {} 36 | }, 37 | reset: async () => { 38 | localStorage.clear(); 39 | }, 40 | }; 41 | } 42 | 43 | getCode() { 44 | return Promise.reject(new Error("Cannot generate code in browser")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/_test.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { Client } from "./Client"; 3 | 4 | console.time(); 5 | const client = new Client({ debug: true }); 6 | 7 | await client.login(); 8 | const uid = client.dashboard?.bacheca.find((e) => e.listaAllegati.length) 9 | ?.listaAllegati[0]?.pk; 10 | 11 | await Promise.allSettled([ 12 | client.getCorsiRecupero(), 13 | client 14 | .getCurriculum() 15 | .then((c) => 16 | Promise.allSettled([ 17 | client.getStoricoBacheca(c.at(-1)!.pkScheda), 18 | client.getStoricoBachecaAlunno(c.at(-1)!.pkScheda), 19 | ]), 20 | ), 21 | client.getDettagliProfilo(), 22 | client.getOrarioGiornaliero(), 23 | client.getPCTOData(), 24 | client.getRicevimenti(), 25 | client.getTasse(), 26 | client.getVotiScrutinio(), 27 | uid && client.getLinkAllegato(uid), 28 | ]); 29 | await client.logOut(); 30 | console.timeEnd(); 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Client"; 2 | export * from "./types"; 3 | export * from "./util"; 4 | export * from "./util/getCode"; 5 | export * from "./util/importData"; 6 | export * from "./util/writeToFile"; 7 | -------------------------------------------------------------------------------- /src/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchemaType } from "ajv"; 2 | import Ajv from "ajv"; 3 | import { mkdir, stat } from "node:fs/promises"; 4 | import { tmpdir } from "node:os"; 5 | import { join } from "node:path"; 6 | import type { 7 | APIBacheca, 8 | APIBachecaAlunno, 9 | APICorsiRecupero, 10 | APICurriculum, 11 | APIDashboard, 12 | APIDettagliProfilo, 13 | APIDownloadAllegato, 14 | APILogin, 15 | APIOrarioGiornaliero, 16 | APIPCTO, 17 | APIProfilo, 18 | APIRicevimenti, 19 | APIRicevutaTelematica, 20 | APITasse, 21 | APIToken, 22 | APIVotiScrutinio, 23 | APIWhat, 24 | } from "../types"; 25 | import { writeToFile } from "../util/writeToFile"; 26 | import { 27 | allRequired, 28 | any, 29 | apiOperation, 30 | apiResponse, 31 | array, 32 | base, 33 | boolean, 34 | merge, 35 | nullableNumber, 36 | nullableString, 37 | number, 38 | record, 39 | string, 40 | } from "./utilityTypes"; 41 | 42 | const ajv = new Ajv({ 43 | allErrors: true, 44 | strictRequired: "log", 45 | verbose: true, 46 | }); 47 | const validate = (name: string, schema: JSONSchemaType) => { 48 | const func = ajv.compile(schema); 49 | 50 | return (data: unknown) => { 51 | setTimeout(async () => { 52 | try { 53 | func(data); 54 | const { errors } = func; 55 | 56 | if (errors) { 57 | const fileName = `${name}-${Date.now()}`; 58 | const errorsPath = join(tmpdir(), "argo"); 59 | const stats = await stat(errorsPath).catch(() => mkdir(errorsPath)); 60 | 61 | if (!stats || stats.isDirectory()) 62 | await writeToFile( 63 | fileName, 64 | { data, error: ajv.errorsText(errors) }, 65 | errorsPath, 66 | ); 67 | console.warn( 68 | `\x1b[33m⚠️ Received an unexpected ${name}\n⚠️ Please, create an issue on https://github.com/DTrombett/portaleargo-api/issues providing the data received from the API and the errors saved in ${join( 69 | errorsPath, 70 | fileName, 71 | )}.json (remember to hide eventual sensitive data)\x1b[0m`, 72 | ); 73 | } 74 | } catch (err) { 75 | console.error(err); 76 | } 77 | }); 78 | }; 79 | }; 80 | 81 | export const validateToken = validate("token", { 82 | ...base, 83 | properties: { 84 | access_token: string, 85 | expires_in: number, 86 | id_token: string, 87 | refresh_token: string, 88 | scope: string, 89 | token_type: string, 90 | error: string, 91 | error_description: string, 92 | }, 93 | oneOf: [ 94 | allRequired({ 95 | access_token: string, 96 | expires_in: number, 97 | id_token: string, 98 | refresh_token: string, 99 | scope: string, 100 | token_type: string, 101 | }), 102 | allRequired({ 103 | error: string, 104 | error_description: string, 105 | }), 106 | ], 107 | }); 108 | export const validateLogin = validate( 109 | "loginData", 110 | merge, Pick>( 111 | apiResponse({ 112 | type: "array", 113 | minItems: 1, 114 | maxItems: 1, 115 | additionalItems: false, 116 | items: [ 117 | allRequired({ 118 | codMin: string, 119 | isPrimoAccesso: boolean, 120 | isResetPassword: boolean, 121 | isSpid: boolean, 122 | profiloDisabilitato: boolean, 123 | token: string, 124 | username: string, 125 | opzioni: array( 126 | allRequired({ 127 | chiave: string, 128 | valore: boolean, 129 | }), 130 | ), 131 | }), 132 | ], 133 | }), 134 | allRequired({ total: number }), 135 | ), 136 | ); 137 | export const validateProfilo = validate( 138 | "profilo", 139 | apiResponse( 140 | allRequired({ 141 | resetPassword: boolean, 142 | ultimoCambioPwd: nullableString, 143 | profiloDisabilitato: boolean, 144 | isSpid: boolean, 145 | primoAccesso: boolean, 146 | profiloStorico: boolean, 147 | alunno: allRequired({ 148 | cognome: string, 149 | desEmail: nullableString, 150 | isUltimaClasse: boolean, 151 | maggiorenne: boolean, 152 | nome: string, 153 | nominativo: string, 154 | pk: string, 155 | }), 156 | anno: allRequired({ 157 | anno: string, 158 | dataFine: string, 159 | dataInizio: string, 160 | }), 161 | genitore: allRequired({ 162 | desEMail: string, 163 | nominativo: string, 164 | pk: string, 165 | }), 166 | scheda: allRequired({ 167 | pk: string, 168 | classe: allRequired({ 169 | pk: string, 170 | desDenominazione: string, 171 | desSezione: string, 172 | }), 173 | corso: allRequired({ 174 | descrizione: string, 175 | pk: string, 176 | }), 177 | scuola: allRequired({ 178 | descrizione: string, 179 | desOrdine: string, 180 | pk: string, 181 | }), 182 | sede: allRequired({ 183 | descrizione: string, 184 | pk: string, 185 | }), 186 | }), 187 | }), 188 | ), 189 | ); 190 | export const validateDettagliProfilo = validate( 191 | "dettagliProfilo", 192 | apiResponse( 193 | allRequired({ 194 | utente: allRequired({ flgUtente: string }), 195 | genitore: allRequired({ 196 | flgSesso: string, 197 | desCognome: string, 198 | desEMail: string, 199 | desCellulare: nullableString, 200 | desTelefono: string, 201 | desNome: string, 202 | datNascita: string, 203 | }), 204 | alunno: allRequired({ 205 | cognome: string, 206 | desCellulare: nullableString, 207 | desCf: string, 208 | datNascita: string, 209 | desCap: string, 210 | desComuneResidenza: string, 211 | nome: string, 212 | desComuneNascita: string, 213 | desCapResidenza: string, 214 | cittadinanza: string, 215 | desIndirizzoRecapito: string, 216 | desEMail: nullableString, 217 | nominativo: string, 218 | desVia: string, 219 | desTelefono: string, 220 | sesso: string, 221 | desComuneRecapito: string, 222 | }), 223 | }), 224 | ), 225 | ); 226 | export const validateWhat = validate( 227 | "what", 228 | apiResponse( 229 | allRequired({ 230 | dati: { 231 | type: "array", 232 | minItems: 1, 233 | maxItems: 1, 234 | additionalItems: false, 235 | items: [ 236 | allRequired({ 237 | forceLogin: boolean, 238 | isModificato: boolean, 239 | pk: string, 240 | mostraPallino: boolean, 241 | differenzaSchede: boolean, 242 | profiloStorico: boolean, 243 | alunno: allRequired({ 244 | isUltimaClasse: boolean, 245 | nominativo: string, 246 | cognome: string, 247 | nome: string, 248 | pk: string, 249 | maggiorenne: boolean, 250 | desEmail: nullableString, 251 | }), 252 | scheda: allRequired({ 253 | aggiornaSchedaPK: boolean, 254 | classe: allRequired({ 255 | pk: string, 256 | desDenominazione: string, 257 | desSezione: string, 258 | }), 259 | dataInizio: string, 260 | anno: number, 261 | corso: allRequired({ 262 | descrizione: string, 263 | pk: string, 264 | }), 265 | sede: allRequired({ 266 | descrizione: string, 267 | pk: string, 268 | }), 269 | scuola: allRequired({ 270 | desOrdine: string, 271 | descrizione: string, 272 | pk: string, 273 | }), 274 | dataFine: string, 275 | pk: string, 276 | }), 277 | }), 278 | ], 279 | }, 280 | }), 281 | ), 282 | ); 283 | export const validateOrarioGiornaliero = validate( 284 | "orario", 285 | apiResponse( 286 | allRequired({ 287 | dati: record( 288 | string, 289 | { 290 | type: "array", 291 | items: { 292 | ...base, 293 | properties: { 294 | numOra: number, 295 | mostra: boolean, 296 | desCognome: string, 297 | desNome: string, 298 | docente: string, 299 | materia: string, 300 | pk: { ...string, nullable: true }, 301 | scuAnagrafePK: { ...string, nullable: true }, 302 | desDenominazione: string, 303 | desEmail: string, 304 | desSezione: string, 305 | ora: nullableString, 306 | }, 307 | required: [ 308 | "numOra", 309 | "mostra", 310 | "desCognome", 311 | "desNome", 312 | "docente", 313 | "materia", 314 | "desDenominazione", 315 | "desEmail", 316 | "desSezione", 317 | "ora", 318 | ], 319 | }, 320 | }, 321 | ), 322 | }), 323 | ), 324 | ); 325 | export const validateDownloadAllegato = validate( 326 | "downloadAllegato", 327 | { 328 | ...base, 329 | required: ["success"], 330 | properties: { success: boolean, url: string, msg: string }, 331 | oneOf: [ 332 | { required: ["msg"], properties: { msg: string } }, 333 | { required: ["url"], properties: { url: string } }, 334 | ], 335 | }, 336 | ); 337 | export const validateVotiScrutinio = validate( 338 | "votiScrutinio", 339 | apiResponse( 340 | allRequired({ 341 | votiScrutinio: array( 342 | allRequired({ 343 | pk: string, 344 | periodi: array( 345 | allRequired({ 346 | desDescrizione: string, 347 | materie: array(string), 348 | suddivisione: string, 349 | votiGiudizi: boolean, 350 | scrutinioFinale: boolean, 351 | }), 352 | ), 353 | }), 354 | ), 355 | }), 356 | ), 357 | ); 358 | export const validateRicevimenti = validate( 359 | "ricevimenti", 360 | apiResponse( 361 | allRequired({ 362 | tipoAccesso: string, 363 | genitoreOAlunno: array( 364 | allRequired({ 365 | desEMail: string, 366 | nominativo: string, 367 | pk: string, 368 | telefono: string, 369 | }), 370 | ), 371 | prenotazioni: array( 372 | allRequired({ 373 | operazione: string, 374 | datEvento: string, 375 | prenotazione: allRequired({ 376 | prgScuola: number, 377 | datPrenotazione: string, 378 | numPrenotazione: nullableNumber, 379 | prgAlunno: number, 380 | genitore: string, 381 | numMax: number, 382 | orarioPrenotazione: string, 383 | prgGenitore: number, 384 | flgAnnullato: nullableString, 385 | flgAnnullatoDa: nullableString, 386 | desTelefonoGenitore: string, 387 | flgTipo: nullableString, 388 | datAnnullamento: nullableString, 389 | desUrl: nullableString, 390 | pk: string, 391 | genitorePK: string, 392 | desEMailGenitore: string, 393 | numPrenotazioni: nullableNumber, 394 | }), 395 | disponibilita: allRequired({ 396 | ora_Fine: string, 397 | desNota: string, 398 | datDisponibilita: string, 399 | desUrl: string, 400 | numMax: number, 401 | ora_Inizio: string, 402 | flgAttivo: string, 403 | desLuogoRicevimento: string, 404 | pk: string, 405 | }), 406 | docente: allRequired({ 407 | desCognome: string, 408 | desNome: string, 409 | pk: string, 410 | desEmail: nullableString, 411 | }), 412 | }), 413 | ), 414 | disponibilita: record< 415 | string, 416 | APIRicevimenti["data"]["disponibilita"][string] 417 | >(string, { 418 | type: "array", 419 | items: allRequired({ 420 | numMax: number, 421 | desNota: string, 422 | docente: allRequired({ 423 | desCognome: string, 424 | desNome: string, 425 | pk: string, 426 | desEmail: nullableString, 427 | }), 428 | numPrenotazioniAnnullate: nullableNumber, 429 | flgAttivo: string, 430 | oraFine: string, 431 | indisponibilita: nullableString, 432 | datInizioPrenotazione: string, 433 | desUrl: string, 434 | unaTantum: string, 435 | oraInizioPrenotazione: string, 436 | datScadenza: string, 437 | desLuogoRicevimento: string, 438 | oraInizio: string, 439 | pk: string, 440 | flgMostraEmail: string, 441 | desEMailDocente: string, 442 | numPrenotazioni: number, 443 | }), 444 | }), 445 | }), 446 | ), 447 | ); 448 | export const validateTasse = validate( 449 | "tasse", 450 | merge< 451 | Omit, 452 | Pick 453 | >( 454 | apiResponse( 455 | array( 456 | allRequired({ 457 | importoPrevisto: string, 458 | dataPagamento: nullableString, 459 | dataCreazione: nullableString, 460 | scadenza: string, 461 | rptPresent: boolean, 462 | rata: string, 463 | iuv: nullableString, 464 | importoTassa: string, 465 | stato: string, 466 | descrizione: string, 467 | debitore: string, 468 | importoPagato: nullableString, 469 | pagabileOltreScadenza: boolean, 470 | rtPresent: boolean, 471 | isPagoOnLine: boolean, 472 | status: string, 473 | listaSingoliPagamenti: { 474 | type: "array", 475 | items: allRequired({ 476 | descrizione: string, 477 | importoPrevisto: string, 478 | importoTassa: string, 479 | }), 480 | ...({ nullable: true } as object), 481 | }, 482 | }), 483 | ), 484 | ), 485 | allRequired({ 486 | isPagOnlineAttivo: boolean, 487 | }), 488 | ), 489 | ); 490 | export const validatePCTO = validate( 491 | "pcto", 492 | apiResponse( 493 | allRequired({ 494 | pcto: array( 495 | allRequired({ 496 | percorsi: array(any), 497 | pk: string, 498 | }), 499 | ), 500 | }), 501 | ), 502 | ); 503 | export const validateCorsiRecupero = validate( 504 | "corsiRecupero", 505 | apiResponse( 506 | allRequired({ 507 | corsiRecupero: array(any), 508 | periodi: array(any), 509 | }), 510 | ), 511 | ); 512 | export const validateCurriculum = validate( 513 | "curriculum", 514 | apiResponse( 515 | allRequired({ 516 | curriculum: array( 517 | allRequired({ 518 | anno: number, 519 | classe: string, 520 | credito: number, 521 | CVAbilitato: boolean, 522 | esito: { 523 | oneOf: [ 524 | string, 525 | allRequired({ 526 | desDescrizione: string, 527 | numColore: number, 528 | flgPositivo: string, 529 | flgTipoParticolare: nullableString, 530 | tipoEsito: string, 531 | descrizione: string, 532 | icona: string, 533 | codEsito: string, 534 | particolarita: string, 535 | positivo: string, 536 | tipoEsitoParticolare: string, 537 | esitoPK: allRequired({ 538 | codMin: string, 539 | codEsito: string, 540 | }), 541 | }), 542 | ], 543 | }, 544 | isInterruzioneFR: boolean, 545 | isSuperiore: boolean, 546 | media: nullableNumber, 547 | mostraCredito: boolean, 548 | mostraInfo: boolean, 549 | ordineScuola: string, 550 | pkScheda: string, 551 | }), 552 | ), 553 | }), 554 | ), 555 | ); 556 | export const validateRicevutaTelematica = validate( 557 | "ricevuta", 558 | { 559 | ...base, 560 | properties: { 561 | fileName: string, 562 | msg: { ...string, nullable: true }, 563 | success: boolean, 564 | url: string, 565 | }, 566 | required: ["fileName", "success", "url"], 567 | }, 568 | ); 569 | export const validateDashboard = validate( 570 | "dashboard", 571 | apiResponse( 572 | allRequired({ 573 | dati: { 574 | type: "array", 575 | minItems: 1, 576 | maxItems: 1, 577 | additionalItems: false, 578 | items: [ 579 | { 580 | ...base, 581 | properties: { 582 | msg: string, 583 | mediaGenerale: number, 584 | mensa: { type: "object", nullable: true }, 585 | rimuoviDatiLocali: boolean, 586 | ricaricaDati: boolean, 587 | profiloDisabilitato: boolean, 588 | autocertificazione: { type: "object", nullable: true }, 589 | schede: array(any), 590 | prenotazioniAlunni: apiOperation( 591 | allRequired({ 592 | datEvento: string, 593 | docente: allRequired({ 594 | desCognome: string, 595 | desNome: string, 596 | pk: string, 597 | desEmail: string, 598 | }), 599 | disponibilita: allRequired({ 600 | ora_Fine: string, 601 | desNota: string, 602 | datDisponibilita: string, 603 | desUrl: string, 604 | numMax: number, 605 | ora_Inizio: string, 606 | flgAttivo: string, 607 | desLuogoRicevimento: string, 608 | pk: string, 609 | }), 610 | prenotazione: allRequired({ 611 | prgScuola: number, 612 | datPrenotazione: string, 613 | numPrenotazione: nullableNumber, 614 | prgAlunno: number, 615 | genitore: string, 616 | numMax: number, 617 | orarioPrenotazione: string, 618 | prgGenitore: number, 619 | flgAnnullato: nullableString, 620 | flgAnnullatoDa: nullableString, 621 | desTelefonoGenitore: string, 622 | flgTipo: nullableString, 623 | datAnnullamento: nullableString, 624 | desUrl: nullableString, 625 | genitorePK: string, 626 | desEMailGenitore: string, 627 | numPrenotazioni: nullableNumber, 628 | pk: string, 629 | }), 630 | }), 631 | true, 632 | ), 633 | noteDisciplinari: array(any), 634 | pk: string, 635 | classiExtra: boolean, 636 | opzioni: array( 637 | allRequired({ 638 | chiave: string, 639 | valore: boolean, 640 | }), 641 | ), 642 | mediaPerMese: { ...record(string, number), nullable: true }, 643 | listaMaterie: array( 644 | allRequired({ 645 | abbreviazione: string, 646 | scrut: boolean, 647 | codTipo: string, 648 | faMedia: boolean, 649 | materia: string, 650 | pk: string, 651 | }), 652 | ), 653 | listaPeriodi: { 654 | ...array< 655 | NonNullable 656 | >({ 657 | ...base, 658 | properties: { 659 | pkPeriodo: string, 660 | dataInizio: string, 661 | descrizione: string, 662 | datInizio: { type: "string", nullable: true }, 663 | votoUnico: boolean, 664 | mediaScrutinio: number, 665 | isMediaScrutinio: boolean, 666 | dataFine: string, 667 | datFine: { type: "string", nullable: true }, 668 | codPeriodo: string, 669 | isScrutinioFinale: boolean, 670 | }, 671 | required: [ 672 | "codPeriodo", 673 | "dataFine", 674 | "dataInizio", 675 | "descrizione", 676 | "isMediaScrutinio", 677 | "isScrutinioFinale", 678 | "mediaScrutinio", 679 | "pkPeriodo", 680 | "votoUnico", 681 | ], 682 | }), 683 | nullable: true, 684 | }, 685 | fileCondivisi: allRequired({ 686 | fileAlunniScollegati: array(any), 687 | listaFile: array(any), 688 | }), 689 | listaDocentiClasse: array( 690 | allRequired({ 691 | desCognome: string, 692 | materie: array(string), 693 | desNome: string, 694 | pk: string, 695 | desEmail: string, 696 | }), 697 | ), 698 | mediaPerPeriodo: { 699 | ...record( 700 | string, 701 | allRequired({ 702 | mediaGenerale: number, 703 | mediaMese: record(string, number), 704 | listaMaterie: record( 705 | string, 706 | allRequired({ 707 | sommaValutazioniOrale: number, 708 | numValutazioniOrale: number, 709 | mediaMateria: number, 710 | mediaScritta: number, 711 | sumValori: number, 712 | numValori: number, 713 | numVoti: number, 714 | numValutazioniScritto: number, 715 | sommaValutazioniScritto: number, 716 | mediaOrale: number, 717 | }), 718 | ), 719 | }), 720 | ), 721 | nullable: true, 722 | }, 723 | mediaMaterie: merge< 724 | Record< 725 | string, 726 | { 727 | sommaValutazioniOrale: number; 728 | numValutazioniOrale: number; 729 | mediaMateria: number; 730 | mediaScritta: number; 731 | sumValori: number; 732 | numValori: number; 733 | numVoti: number; 734 | numValutazioniScritto: number; 735 | sommaValutazioniScritto: number; 736 | mediaOrale: number; 737 | } 738 | >, 739 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 740 | { listaMaterie?: {} } 741 | >( 742 | record( 743 | string, 744 | allRequired({ 745 | sommaValutazioniOrale: number, 746 | numValutazioniOrale: number, 747 | mediaMateria: number, 748 | mediaScritta: number, 749 | sumValori: number, 750 | numValori: number, 751 | numVoti: number, 752 | numValutazioniScritto: number, 753 | sommaValutazioniScritto: number, 754 | mediaOrale: number, 755 | }), 756 | ), 757 | { 758 | ...base, 759 | properties: { 760 | listaMaterie: { type: "object", nullable: true }, 761 | }, 762 | }, 763 | ), 764 | appello: apiOperation( 765 | allRequired({ 766 | datEvento: string, 767 | descrizione: string, 768 | daGiustificare: boolean, 769 | giustificata: string, 770 | data: string, 771 | codEvento: string, 772 | docente: string, 773 | commentoGiustificazione: string, 774 | dataGiustificazione: string, 775 | nota: string, 776 | }), 777 | ), 778 | bacheca: apiOperation( 779 | allRequired({ 780 | datEvento: string, 781 | messaggio: string, 782 | data: string, 783 | pvRichiesta: boolean, 784 | categoria: string, 785 | dataConfermaPresaVisione: string, 786 | url: nullableString, 787 | autore: string, 788 | dataScadenza: nullableString, 789 | adRichiesta: boolean, 790 | isPresaVisione: boolean, 791 | dataConfermaAdesione: string, 792 | listaAllegati: array( 793 | allRequired({ 794 | nomeFile: string, 795 | path: string, 796 | descrizioneFile: nullableString, 797 | pk: string, 798 | url: string, 799 | }), 800 | ), 801 | dataScadAdesione: nullableString, 802 | isPresaAdesioneConfermata: boolean, 803 | }), 804 | ), 805 | bachecaAlunno: apiOperation( 806 | allRequired({ 807 | nomeFile: string, 808 | datEvento: string, 809 | messaggio: string, 810 | data: string, 811 | flgDownloadGenitore: string, 812 | isPresaVisione: boolean, 813 | }), 814 | ), 815 | fuoriClasse: apiOperation( 816 | allRequired({ 817 | datEvento: string, 818 | descrizione: string, 819 | data: string, 820 | docente: string, 821 | nota: string, 822 | frequenzaOnLine: boolean, 823 | }), 824 | ), 825 | promemoria: apiOperation( 826 | allRequired({ 827 | datEvento: string, 828 | desAnnotazioni: string, 829 | pkDocente: string, 830 | flgVisibileFamiglia: string, 831 | datGiorno: string, 832 | docente: string, 833 | oraInizio: string, 834 | oraFine: string, 835 | }), 836 | ), 837 | registro: apiOperation( 838 | allRequired({ 839 | datEvento: string, 840 | isFirmato: boolean, 841 | desUrl: nullableString, 842 | pkDocente: string, 843 | compiti: array( 844 | allRequired({ 845 | compito: string, 846 | dataConsegna: string, 847 | }), 848 | ), 849 | datGiorno: string, 850 | docente: string, 851 | materia: string, 852 | pkMateria: string, 853 | attivita: nullableString, 854 | ora: number, 855 | }), 856 | ), 857 | voti: apiOperation( 858 | allRequired({ 859 | datEvento: string, 860 | pkPeriodo: string, 861 | codCodice: string, 862 | valore: number, 863 | codVotoPratico: string, 864 | docente: string, 865 | pkMateria: string, 866 | tipoValutazione: nullableString, 867 | prgVoto: number, 868 | descrizioneProva: string, 869 | faMenoMedia: string, 870 | pkDocente: string, 871 | descrizioneVoto: string, 872 | codTipo: string, 873 | datGiorno: string, 874 | mese: number, 875 | numMedia: number, 876 | desMateria: string, 877 | materiaLight: allRequired({ 878 | scuMateriaPK: allRequired({ 879 | codMin: string, 880 | prgScuola: number, 881 | numAnno: number, 882 | prgMateria: number, 883 | }), 884 | codMateria: string, 885 | desDescrizione: string, 886 | desDescrAbbrev: string, 887 | codSuddivisione: string, 888 | codTipo: string, 889 | flgConcorreMedia: string, 890 | codAggrDisciplina: nullableString, 891 | flgLezioniIndividuali: nullableString, 892 | codAggrInvalsi: nullableString, 893 | codMinisteriale: nullableString, 894 | icona: string, 895 | descrizione: nullableString, 896 | conInsufficienze: boolean, 897 | selezionata: boolean, 898 | prgMateria: number, 899 | tipo: string, 900 | articolata: string, 901 | lezioniIndividuali: boolean, 902 | idmateria: string, 903 | codEDescrizioneMateria: string, 904 | tipoOnGrid: string, 905 | }), 906 | desCommento: string, 907 | }), 908 | ), 909 | }, 910 | required: [ 911 | "appello", 912 | "bacheca", 913 | "bachecaAlunno", 914 | "classiExtra", 915 | "fileCondivisi", 916 | "fuoriClasse", 917 | "listaDocentiClasse", 918 | "listaMaterie", 919 | "mediaGenerale", 920 | "mediaMaterie", 921 | "msg", 922 | "noteDisciplinari", 923 | "opzioni", 924 | "pk", 925 | "prenotazioniAlunni", 926 | "profiloDisabilitato", 927 | "promemoria", 928 | "registro", 929 | "ricaricaDati", 930 | "rimuoviDatiLocali", 931 | "schede", 932 | "voti", 933 | ], 934 | }, 935 | ], 936 | }, 937 | }), 938 | ), 939 | ); 940 | export const validateBacheca = validate( 941 | "bacheca", 942 | apiResponse( 943 | allRequired({ 944 | bacheca: apiOperation( 945 | allRequired({ 946 | datEvento: string, 947 | messaggio: string, 948 | data: string, 949 | pvRichiesta: boolean, 950 | categoria: string, 951 | dataConfermaPresaVisione: string, 952 | url: nullableString, 953 | autore: string, 954 | dataScadenza: nullableString, 955 | adRichiesta: boolean, 956 | isPresaVisione: boolean, 957 | dataConfermaAdesione: string, 958 | listaAllegati: array( 959 | allRequired({ 960 | nomeFile: string, 961 | path: string, 962 | descrizioneFile: nullableString, 963 | pk: string, 964 | url: string, 965 | }), 966 | ), 967 | dataScadAdesione: nullableString, 968 | isPresaAdesioneConfermata: boolean, 969 | }), 970 | ), 971 | }), 972 | ), 973 | ); 974 | export const validateBachecaAlunno = validate( 975 | "bachecaAlunno", 976 | apiResponse( 977 | allRequired({ 978 | bachecaAlunno: apiOperation( 979 | allRequired({ 980 | nomeFile: string, 981 | datEvento: string, 982 | messaggio: string, 983 | data: string, 984 | flgDownloadGenitore: string, 985 | isPresaVisione: boolean, 986 | }), 987 | ), 988 | }), 989 | ), 990 | ); 991 | -------------------------------------------------------------------------------- /src/schemas/utilityTypes.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchemaType } from "ajv"; 2 | import type { APIOperation, APIResponse } from "../types"; 3 | 4 | export const base = { type: "object", additionalProperties: false } as const; 5 | export const allRequired = < 6 | T, 7 | P extends JSONSchemaType["properties"] = JSONSchemaType["properties"], 8 | >( 9 | properties: P, 10 | ): typeof base & { 11 | properties: P; 12 | required: (keyof P)[]; 13 | } => ({ 14 | ...base, 15 | properties, 16 | required: Object.keys(properties), 17 | }); 18 | export const boolean: JSONSchemaType = { type: "boolean" }; 19 | export const string: JSONSchemaType = { type: "string" }; 20 | export const number: JSONSchemaType = { type: "number" }; 21 | export const nullableString: JSONSchemaType = { 22 | type: "string", 23 | nullable: true, 24 | }; 25 | export const nullableNumber: JSONSchemaType = { 26 | type: "number", 27 | nullable: true, 28 | }; 29 | export const record = ( 30 | name: JSONSchemaType, 31 | value: JSONSchemaType, 32 | ): JSONSchemaType> => ({ 33 | type: "object", 34 | required: [], 35 | propertyNames: name, 36 | additionalProperties: value, 37 | }); 38 | export const apiResponse = ( 39 | data: T extends APIResponse ? JSONSchemaType : never, 40 | ): JSONSchemaType => ({ 41 | ...base, 42 | properties: { 43 | success: boolean, 44 | msg: nullableString, 45 | data, 46 | }, 47 | required: ["success", "data"], 48 | }); 49 | export const array = ( 50 | items: JSONSchemaType[number]>, 51 | options?: Partial>, 52 | ) => 53 | ({ 54 | type: "array", 55 | items, 56 | ...options, 57 | }) as JSONSchemaType; 58 | export const merge = ( 59 | first: JSONSchemaType, 60 | second: JSONSchemaType, 61 | ) => 62 | ({ 63 | ...second, 64 | ...first, 65 | properties: { ...second.properties, ...first.properties }, 66 | required: [...(second.required ?? []), ...(first.required ?? [])], 67 | }) as JSONSchemaType; 68 | export const apiOperation = ( 69 | items: JSONSchemaType, 70 | omitPk = false as P, 71 | ): JSONSchemaType< 72 | (P extends true ? Omit, "pk"> : APIOperation)[] 73 | > => 74 | array({ 75 | type: "object", 76 | properties: { 77 | pk: string, 78 | operazione: { type: "string", enum: ["D", "I"], nullable: true }, 79 | }, 80 | required: omitPk ? undefined : ["pk"], 81 | oneOf: [ 82 | { 83 | additionalProperties: false, 84 | properties: { pk: string, operazione: { const: "D" } }, 85 | required: ["operazione", ...(omitPk ? [] : ["pk"])], 86 | }, 87 | { 88 | ...items, 89 | properties: { 90 | ...items.properties, 91 | pk: string, 92 | operazione: { type: "string", enum: ["I"], nullable: true }, 93 | }, 94 | required: [...(items.required ?? []), ...(omitPk ? [] : ["pk"])], 95 | }, 96 | ], 97 | } as JSONSchemaType>); 98 | export const any = {} as JSONSchemaType; 99 | export const arrayOfAny: JSONSchemaType = array(any); 100 | -------------------------------------------------------------------------------- /src/types/apiTypes.ts: -------------------------------------------------------------------------------- 1 | import type { Json } from "./general"; 2 | 3 | export type APIResponse = { 4 | success: boolean; 5 | msg?: string | null; 6 | data: T; 7 | }; 8 | export type APIOperation = (P extends true 9 | ? { 10 | pk?: undefined; 11 | } 12 | : { 13 | pk: string; 14 | }) & 15 | ( 16 | | { 17 | operazione: "D"; 18 | pk: string; 19 | } 20 | | (T & 21 | (P extends true 22 | ? { 23 | operazione: "I"; 24 | } 25 | : { 26 | operazione?: "I"; 27 | })) 28 | ); 29 | export type APIToken = 30 | | { 31 | access_token: string; 32 | expires_in: number; 33 | id_token: string; 34 | refresh_token: string; 35 | scope: string; 36 | token_type: string; 37 | } 38 | | { 39 | error: string; 40 | error_description: string; 41 | }; 42 | export type APILogin = APIResponse< 43 | [ 44 | { 45 | codMin: string; 46 | opzioni: { 47 | valore: boolean; 48 | chiave: string; 49 | }[]; 50 | isPrimoAccesso: boolean; 51 | profiloDisabilitato: boolean; 52 | isResetPassword: boolean; 53 | isSpid: boolean; 54 | token: string; 55 | username: string; 56 | }, 57 | ] 58 | > & { total: number }; 59 | export type APIProfilo = APIResponse<{ 60 | resetPassword: boolean; 61 | ultimoCambioPwd: string | null; 62 | anno: { 63 | dataInizio: string; 64 | anno: string; 65 | dataFine: string; 66 | }; 67 | genitore: { 68 | desEMail: string; 69 | nominativo: string; 70 | pk: string; 71 | }; 72 | profiloDisabilitato: boolean; 73 | isSpid: boolean; 74 | alunno: { 75 | isUltimaClasse: boolean; 76 | nominativo: string; 77 | cognome: string; 78 | nome: string; 79 | pk: string; 80 | maggiorenne: boolean; 81 | desEmail: string | null; 82 | }; 83 | scheda: { 84 | classe: { 85 | pk: string; 86 | desDenominazione: string; 87 | desSezione: string; 88 | }; 89 | corso: { 90 | descrizione: string; 91 | pk: string; 92 | }; 93 | sede: { 94 | descrizione: string; 95 | pk: string; 96 | }; 97 | scuola: { 98 | desOrdine: string; 99 | descrizione: string; 100 | pk: string; 101 | }; 102 | pk: string; 103 | }; 104 | primoAccesso: boolean; 105 | profiloStorico: boolean; 106 | }>; 107 | export type APIDashboard = APIResponse<{ 108 | dati: [ 109 | { 110 | fuoriClasse: APIOperation<{ 111 | datEvento: string; 112 | descrizione: string; 113 | data: string; 114 | docente: string; 115 | nota: string; 116 | frequenzaOnLine: boolean; 117 | }>[]; 118 | msg: string; 119 | opzioni: { 120 | valore: boolean; 121 | chiave: string; 122 | }[]; 123 | mediaGenerale: number; 124 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 125 | mensa?: {}; 126 | mediaPerMese?: Record; 127 | listaMaterie: { 128 | abbreviazione: string; 129 | scrut: boolean; 130 | codTipo: string; 131 | faMedia: boolean; 132 | materia: string; 133 | pk: string; 134 | }[]; 135 | rimuoviDatiLocali: boolean; 136 | listaPeriodi?: { 137 | pkPeriodo: string; 138 | dataInizio: string; 139 | descrizione: string; 140 | datInizio?: string; 141 | votoUnico: boolean; 142 | mediaScrutinio: number; 143 | isMediaScrutinio: boolean; 144 | dataFine: string; 145 | datFine?: string; 146 | codPeriodo: string; 147 | isScrutinioFinale: boolean; 148 | }[]; 149 | promemoria: APIOperation<{ 150 | datEvento: string; 151 | desAnnotazioni: string; 152 | pkDocente: string; 153 | flgVisibileFamiglia: string; 154 | datGiorno: string; 155 | docente: string; 156 | oraInizio: string; 157 | oraFine: string; 158 | }>[]; 159 | bacheca: APIOperation<{ 160 | datEvento: string; 161 | messaggio: string; 162 | data: string; 163 | pvRichiesta: boolean; 164 | categoria: string; 165 | dataConfermaPresaVisione: string; 166 | url: string | null; 167 | autore: string; 168 | dataScadenza: string | null; 169 | adRichiesta: boolean; 170 | isPresaVisione: boolean; 171 | dataConfermaAdesione: string; 172 | listaAllegati: { 173 | nomeFile: string; 174 | path: string; 175 | descrizioneFile: string | null; 176 | pk: string; 177 | url: string; 178 | }[]; 179 | dataScadAdesione: string | null; 180 | isPresaAdesioneConfermata: boolean; 181 | }>[]; 182 | fileCondivisi: { 183 | fileAlunniScollegati: any[]; 184 | listaFile: any[]; 185 | }; 186 | voti: APIOperation<{ 187 | datEvento: string; 188 | pkPeriodo: string; 189 | codCodice: string; 190 | valore: number; 191 | codVotoPratico: string; 192 | docente: string; 193 | pkMateria: string; 194 | tipoValutazione: string | null; 195 | prgVoto: number; 196 | descrizioneProva: string; 197 | faMenoMedia: string; 198 | pkDocente: string; 199 | descrizioneVoto: string; 200 | codTipo: string; 201 | datGiorno: string; 202 | mese: number; 203 | numMedia: number; 204 | desMateria: string; 205 | materiaLight: { 206 | scuMateriaPK: { 207 | codMin: string; 208 | prgScuola: number; 209 | numAnno: number; 210 | prgMateria: number; 211 | }; 212 | codMateria: string; 213 | desDescrizione: string; 214 | desDescrAbbrev: string; 215 | codSuddivisione: string; 216 | codTipo: string; 217 | flgConcorreMedia: string; 218 | codAggrDisciplina: string | null; 219 | flgLezioniIndividuali: string | null; 220 | codAggrInvalsi: string | null; 221 | codMinisteriale: string | null; 222 | icona: string; 223 | descrizione: string | null; 224 | conInsufficienze: boolean; 225 | selezionata: boolean; 226 | prgMateria: number; 227 | tipo: string; 228 | articolata: string; 229 | lezioniIndividuali: boolean; 230 | idmateria: string; 231 | codEDescrizioneMateria: string; 232 | tipoOnGrid: string; 233 | }; 234 | desCommento: string; 235 | }>[]; 236 | ricaricaDati: boolean; 237 | listaDocentiClasse: { 238 | desCognome: string; 239 | materie: string[]; 240 | desNome: string; 241 | pk: string; 242 | desEmail: string; 243 | }[]; 244 | bachecaAlunno: APIOperation<{ 245 | nomeFile: string; 246 | datEvento: string; 247 | messaggio: string; 248 | data: string; 249 | flgDownloadGenitore: string; 250 | isPresaVisione: boolean; 251 | }>[]; 252 | profiloDisabilitato: boolean; 253 | mediaPerPeriodo?: Record< 254 | string, 255 | { 256 | mediaGenerale: number; 257 | listaMaterie: Record< 258 | string, 259 | { 260 | sommaValutazioniOrale: number; 261 | numValutazioniOrale: number; 262 | mediaMateria: number; 263 | mediaScritta: number; 264 | sumValori: number; 265 | numValori: number; 266 | numVoti: number; 267 | numValutazioniScritto: number; 268 | sommaValutazioniScritto: number; 269 | mediaOrale: number; 270 | } 271 | >; 272 | mediaMese: Record; 273 | } 274 | >; 275 | mediaMaterie: Record< 276 | string, 277 | { 278 | sommaValutazioniOrale: number; 279 | numValutazioniOrale: number; 280 | mediaMateria: number; 281 | mediaScritta: number; 282 | sumValori: number; 283 | numValori: number; 284 | numVoti: number; 285 | numValutazioniScritto: number; 286 | sommaValutazioniScritto: number; 287 | mediaOrale: number; 288 | } 289 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 290 | > & { listaMaterie?: {} }; 291 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 292 | autocertificazione?: {}; 293 | registro: APIOperation<{ 294 | datEvento: string; 295 | isFirmato: boolean; 296 | desUrl: string | null; 297 | pkDocente: string; 298 | compiti: { 299 | compito: string; 300 | dataConsegna: string; 301 | }[]; 302 | datGiorno: string; 303 | docente: string; 304 | materia: string; 305 | pkMateria: string; 306 | attivita: string | null; 307 | ora: number; 308 | }>[]; 309 | schede: any[]; 310 | prenotazioniAlunni: APIOperation< 311 | { 312 | datEvento: string; 313 | prenotazione: { 314 | prgScuola: number; 315 | datPrenotazione: string; 316 | numPrenotazione: number | null; 317 | prgAlunno: number; 318 | genitore: string; 319 | numMax: number; 320 | orarioPrenotazione: string; 321 | prgGenitore: number; 322 | flgAnnullato: string | null; 323 | flgAnnullatoDa: string | null; 324 | desTelefonoGenitore: string; 325 | flgTipo: string | null; 326 | datAnnullamento: string | null; 327 | desUrl: string | null; 328 | genitorePK: string; 329 | desEMailGenitore: string; 330 | numPrenotazioni: number | null; 331 | pk: string; 332 | }; 333 | disponibilita: { 334 | ora_Fine: string; 335 | desNota: string; 336 | datDisponibilita: string; 337 | desUrl: string; 338 | numMax: number; 339 | ora_Inizio: string; 340 | flgAttivo: string; 341 | desLuogoRicevimento: string; 342 | pk: string; 343 | }; 344 | docente: { 345 | desCognome: string; 346 | desNome: string; 347 | pk: string; 348 | desEmail: string; 349 | }; 350 | }, 351 | true 352 | >[]; 353 | noteDisciplinari: any[]; 354 | pk: string; 355 | appello: APIOperation<{ 356 | datEvento: string; 357 | descrizione: string; 358 | daGiustificare: boolean; 359 | giustificata: string; 360 | data: string; 361 | codEvento: string; 362 | docente: string; 363 | commentoGiustificazione: string; 364 | dataGiustificazione: string; 365 | nota: string; 366 | }>[]; 367 | classiExtra: boolean; 368 | }, 369 | ]; 370 | }>; 371 | export type APIDettagliProfilo = APIResponse<{ 372 | utente: { 373 | flgUtente: string; 374 | }; 375 | genitore: { 376 | flgSesso: string; 377 | desCognome: string; 378 | desEMail: string; 379 | desCellulare: string | null; 380 | desTelefono: string; 381 | desNome: string; 382 | datNascita: string; 383 | }; 384 | alunno: { 385 | cognome: string; 386 | desCellulare: string | null; 387 | desCf: string; 388 | datNascita: string; 389 | desCap: string; 390 | desComuneResidenza: string; 391 | nome: string; 392 | desComuneNascita: string; 393 | desCapResidenza: string; 394 | cittadinanza: string; 395 | desIndirizzoRecapito: string; 396 | desEMail: string | null; 397 | nominativo: string; 398 | desVia: string; 399 | desTelefono: string; 400 | sesso: string; 401 | desComuneRecapito: string; 402 | }; 403 | }>; 404 | export type APIWhat = APIResponse<{ 405 | dati: [ 406 | { 407 | forceLogin: boolean; 408 | isModificato: boolean; 409 | pk: string; 410 | alunno: { 411 | isUltimaClasse: boolean; 412 | nominativo: string; 413 | cognome: string; 414 | nome: string; 415 | pk: string; 416 | maggiorenne: boolean; 417 | desEmail: string | null; 418 | }; 419 | mostraPallino: boolean; 420 | scheda: { 421 | aggiornaSchedaPK: boolean; 422 | classe: { 423 | pk: string; 424 | desDenominazione: string; 425 | desSezione: string; 426 | }; 427 | dataInizio: string; 428 | anno: number; 429 | corso: { 430 | descrizione: string; 431 | pk: string; 432 | }; 433 | sede: { 434 | descrizione: string; 435 | pk: string; 436 | }; 437 | scuola: { 438 | desOrdine: string; 439 | descrizione: string; 440 | pk: string; 441 | }; 442 | dataFine: string; 443 | pk: string; 444 | }; 445 | differenzaSchede: boolean; 446 | profiloStorico: boolean; 447 | }, 448 | ]; 449 | }>; 450 | export type APIOrarioGiornaliero = APIResponse<{ 451 | dati: Record< 452 | string, 453 | { 454 | numOra: number; 455 | mostra: boolean; 456 | desCognome: string; 457 | desNome: string; 458 | docente: string; 459 | materia: string; 460 | pk?: string; 461 | scuAnagrafePK?: string; 462 | desDenominazione: string; 463 | desEmail: string; 464 | desSezione: string; 465 | ora: string | null; 466 | }[] 467 | >; 468 | }>; 469 | export type APIDownloadAllegato = 470 | | { 471 | success: false; 472 | msg: string; 473 | } 474 | | { 475 | success: true; 476 | url: string; 477 | }; 478 | export type APIVotiScrutinio = APIResponse<{ 479 | votiScrutinio: { 480 | periodi: { 481 | desDescrizione: string; 482 | materie: string[]; 483 | suddivisione: string; 484 | votiGiudizi: boolean; 485 | scrutinioFinale: boolean; 486 | }[]; 487 | pk: string; 488 | }[]; 489 | }>; 490 | export type APIRicevimenti = APIResponse<{ 491 | disponibilita: Record< 492 | string, 493 | { 494 | desNota: string; 495 | numMax: number; 496 | docente: { 497 | desCognome: string; 498 | desNome: string; 499 | pk: string; 500 | desEmail: string | null; 501 | }; 502 | numPrenotazioniAnnullate: number | null; 503 | flgAttivo: string; 504 | oraFine: string; 505 | indisponibilita: string | null; 506 | datInizioPrenotazione: string; 507 | desUrl: string; 508 | unaTantum: string; 509 | oraInizioPrenotazione: string; 510 | datScadenza: string; 511 | desLuogoRicevimento: string; 512 | oraInizio: string; 513 | pk: string; 514 | flgMostraEmail: string; 515 | desEMailDocente: string; 516 | numPrenotazioni: number; 517 | }[] 518 | >; 519 | genitoreOAlunno: { 520 | desEMail: string; 521 | nominativo: string; 522 | pk: string; 523 | telefono: string; 524 | }[]; 525 | tipoAccesso: string; 526 | prenotazioni: { 527 | operazione: string; 528 | datEvento: string; 529 | prenotazione: { 530 | prgScuola: number; 531 | datPrenotazione: string; 532 | numPrenotazione: number | null; 533 | prgAlunno: number; 534 | genitore: string; 535 | numMax: number; 536 | orarioPrenotazione: string; 537 | prgGenitore: number; 538 | flgAnnullato: string | null; 539 | flgAnnullatoDa: string | null; 540 | desTelefonoGenitore: string; 541 | flgTipo: string | null; 542 | datAnnullamento: string | null; 543 | desUrl: string | null; 544 | pk: string; 545 | genitorePK: string; 546 | desEMailGenitore: string; 547 | numPrenotazioni: number | null; 548 | }; 549 | disponibilita: { 550 | ora_Fine: string; 551 | desNota: string; 552 | datDisponibilita: string; 553 | desUrl: string; 554 | numMax: number; 555 | ora_Inizio: string; 556 | flgAttivo: string; 557 | desLuogoRicevimento: string; 558 | pk: string; 559 | }; 560 | docente: { 561 | desCognome: string; 562 | desNome: string; 563 | pk: string; 564 | desEmail: string | null; 565 | }; 566 | }[]; 567 | }>; 568 | export type APITasse = APIResponse< 569 | { 570 | importoPrevisto: string; 571 | dataPagamento: string | null; 572 | listaSingoliPagamenti: 573 | | { 574 | importoTassa: string; 575 | descrizione: string; 576 | importoPrevisto: string; 577 | }[] 578 | | null; 579 | dataCreazione: string | null; 580 | scadenza: string; 581 | rptPresent: boolean; 582 | rata: string; 583 | iuv: string | null; 584 | importoTassa: string; 585 | stato: string; 586 | descrizione: string; 587 | debitore: string; 588 | importoPagato: string | null; 589 | pagabileOltreScadenza: boolean; 590 | rtPresent: boolean; 591 | isPagoOnLine: boolean; 592 | status: string; 593 | }[] 594 | > & { 595 | isPagOnlineAttivo: boolean; 596 | }; 597 | export type APIPCTO = APIResponse<{ 598 | pcto: { 599 | percorsi: any[]; 600 | pk: string; 601 | }[]; 602 | }>; 603 | export type APICorsiRecupero = APIResponse<{ 604 | corsiRecupero: any[]; 605 | periodi: any[]; 606 | }>; 607 | export type APICurriculum = APIResponse<{ 608 | curriculum: { 609 | pkScheda: string; 610 | classe: string; 611 | anno: number; 612 | esito: 613 | | "" 614 | | { 615 | esitoPK: { 616 | codMin: string; 617 | codEsito: string; 618 | }; 619 | desDescrizione: string; 620 | numColore: number; 621 | flgPositivo: string; 622 | flgTipoParticolare: string | null; 623 | tipoEsito: string; 624 | descrizione: string; 625 | icona: string; 626 | codEsito: string; 627 | particolarita: string; 628 | positivo: string; 629 | tipoEsitoParticolare: string; 630 | }; 631 | credito: number; 632 | mostraInfo: boolean; 633 | mostraCredito: boolean; 634 | isSuperiore: boolean; 635 | isInterruzioneFR: boolean; 636 | media: number | null; 637 | CVAbilitato: boolean; 638 | ordineScuola: string; 639 | }[]; 640 | }>; 641 | export type APIRicevutaTelematica = { 642 | success: boolean; 643 | fileName: string; 644 | msg?: string; 645 | url: string; 646 | }; 647 | export type APIBacheca = APIResponse< 648 | Pick 649 | >; 650 | export type APIBachecaAlunno = APIResponse< 651 | Pick 652 | >; 653 | -------------------------------------------------------------------------------- /src/types/general.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIDashboard, 3 | APILogin, 4 | APIOperation, 5 | APIProfilo, 6 | APIToken, 7 | } from "./apiTypes"; 8 | 9 | export type ObjectJson = { 10 | [key: string]: Json; 11 | }; 12 | export type Json = 13 | | Json[] 14 | | ObjectJson 15 | | boolean 16 | | number 17 | | string 18 | | null 19 | | undefined; 20 | export type HttpMethod = 21 | | "CONNECT" 22 | | "DELETE" 23 | | "GET" 24 | | "HEAD" 25 | | "OPTIONS" 26 | | "PATCH" 27 | | "POST" 28 | | "PUT" 29 | | "TRACE"; 30 | export type ReadData = { 31 | dashboard: ClearOperations & { 32 | dataAggiornamento: string; 33 | }; 34 | login: APILogin["data"][number]; 35 | profile: APIProfilo["data"]; 36 | token: Token; 37 | }; 38 | export type WriteData = { 39 | dashboard: Dashboard; 40 | login: APILogin["data"][number]; 41 | profile: APIProfilo["data"]; 42 | token: Token; 43 | }; 44 | export type Credentials = { 45 | /** 46 | * Il codice scuola 47 | */ 48 | schoolCode: string; 49 | 50 | /** 51 | * L'username 52 | */ 53 | username: string; 54 | 55 | /** 56 | * La password 57 | */ 58 | password: string; 59 | }; 60 | export type ClearOperations = { 61 | [K in keyof T]: T[K] extends APIOperation[] 62 | ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type 63 | (A & (P extends false ? { pk: string } : {}))[] 64 | : T[K] extends object 65 | ? ClearOperations 66 | : T[K]; 67 | }; 68 | export type Token = Exclude & { 69 | expireDate: Date; 70 | }; 71 | export type Dashboard = ClearOperations< 72 | APIDashboard["data"]["dati"][number] 73 | > & { 74 | dataAggiornamento: Date; 75 | }; 76 | export type ClientOptions = Partial< 77 | Credentials & { 78 | /** 79 | * I dati del token 80 | */ 81 | token: Token; 82 | 83 | /** 84 | * I dati del login 85 | */ 86 | loginData: APILogin["data"][number]; 87 | 88 | /** 89 | * I dati del profilo 90 | */ 91 | profile: APIProfilo["data"]; 92 | 93 | /** 94 | * I dati della dashboard 95 | */ 96 | dashboard: Dashboard; 97 | 98 | /** 99 | * Se scrivere nella console alcuni dati utili per il debug 100 | */ 101 | debug: boolean; 102 | 103 | /** 104 | * Headers aggiuntivi per ogni richiesta API 105 | */ 106 | headers: Record; 107 | 108 | /** 109 | * Le funzioni per leggere e scrivere i dati 110 | */ 111 | dataProvider: { 112 | read?: ( 113 | name: T, 114 | ) => Promise; 115 | write: ( 116 | name: T, 117 | data: WriteData[T], 118 | ) => Promise; 119 | reset: () => Promise; 120 | } | null; 121 | 122 | /** 123 | * La versione di didUp da specificare nell'header. 124 | * * Modificare questa opzione potrebbe creare problemi nell'utilizzo della libreria 125 | */ 126 | version: string; 127 | 128 | /** 129 | * Non controllare il tipo dei dati ricevuti dall'API. 130 | * * Nota che il controllo dei dati viene fatto in maniera asincrona e non blocca o rallenta il processo 131 | */ 132 | noTypeCheck: boolean; 133 | } 134 | >; 135 | export type Jsonify = [D, T] extends [ 136 | true, 137 | { 138 | toJSON(): infer J; 139 | }, 140 | ] 141 | ? Jsonify 142 | : T extends boolean | number | string | null 143 | ? T 144 | : T extends bigint 145 | ? never 146 | : T extends symbol | ((...args: any[]) => any) | undefined 147 | ? undefined 148 | : T extends (infer A)[] 149 | ? Jsonify[] 150 | : { 151 | [K in keyof T as T[K] extends 152 | | bigint 153 | | symbol 154 | | ((...args: any[]) => any) 155 | | undefined 156 | ? never 157 | : K]: Jsonify; 158 | }; 159 | export type LoginLink = { 160 | url: string; 161 | redirectUri: string; 162 | scopes: string[]; 163 | codeVerifier: string; 164 | challenge: string; 165 | clientId: string; 166 | state: string; 167 | nonce: string; 168 | }; 169 | export type ReadyClient = { 170 | token: Token; 171 | loginData: APILogin["data"][number]; 172 | profile: APIProfilo; 173 | }; 174 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./apiTypes"; 2 | export * from "./general"; 3 | -------------------------------------------------------------------------------- /src/util/Constants.ts: -------------------------------------------------------------------------------- 1 | export const clientId = "72fd6dea-d0ab-4bb9-8eaa-3ac24c84886c"; 2 | export const defaultVersion = "1.27.0"; 3 | -------------------------------------------------------------------------------- /src/util/encryptCodeVerifier.ts: -------------------------------------------------------------------------------- 1 | const encoder = new TextEncoder(); 2 | 3 | /** 4 | * Crittografa un codice. 5 | * @param codeVerifier - Il codice 6 | * @returns Il codice crittografato 7 | */ 8 | export const encryptCodeVerifier = async (codeVerifier: string) => 9 | btoa( 10 | String.fromCharCode( 11 | ...new Uint8Array( 12 | await crypto.subtle.digest("SHA-256", encoder.encode(codeVerifier)), 13 | ), 14 | ), 15 | ) 16 | .replace(/\+/g, "-") 17 | .replace(/\//g, "_") 18 | .replace(/=+$/, ""); 19 | -------------------------------------------------------------------------------- /src/util/formatDate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formatta una data dell'API 3 | * @param date - La data 4 | * @returns La data formattata 5 | */ 6 | export const formatDate = (date: Date | number | string) => { 7 | date = new Date(date); 8 | return `${date.getFullYear()}-${(date.getMonth() + 1) 9 | .toString() 10 | .padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")} ${date 11 | .getHours() 12 | .toString() 13 | .padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date 14 | .getSeconds() 15 | .toString() 16 | .padStart(2, "0")}.${date.getMilliseconds().toString().padStart(3, "0")}`; 17 | }; 18 | -------------------------------------------------------------------------------- /src/util/generateLoginLink.ts: -------------------------------------------------------------------------------- 1 | import type { LoginLink } from "../types"; 2 | import { clientId } from "./Constants"; 3 | import { encryptCodeVerifier } from "./encryptCodeVerifier"; 4 | import { randomString } from "./randomString"; 5 | 6 | /** 7 | * Genera un link per il login tramite browser. 8 | * @param param0 - Le opzioni per generare il link 9 | * @returns L'url generato con gli altri dati utilizzati 10 | */ 11 | export const generateLoginLink = async ({ 12 | redirectUri = "it.argosoft.didup.famiglia.new://login-callback", 13 | scopes = ["openid", "offline", "profile", "user.roles", "argo"], 14 | codeVerifier = randomString(43), 15 | challenge, 16 | id = clientId, 17 | state = randomString(22), 18 | nonce = randomString(22), 19 | }: { 20 | redirectUri?: string; 21 | scopes?: string[]; 22 | codeVerifier?: string; 23 | challenge?: string; 24 | id?: string; 25 | state?: string; 26 | nonce?: string; 27 | } = {}): Promise => { 28 | challenge ??= await encryptCodeVerifier(codeVerifier); 29 | return { 30 | url: `https://auth.portaleargo.it/oauth2/auth?redirect_uri=${encodeURIComponent( 31 | redirectUri, 32 | )}&client_id=${id}&response_type=code&prompt=login&state=${state}&nonce=${nonce}&scope=${encodeURIComponent( 33 | scopes.join(" "), 34 | )}&code_challenge=${challenge}&code_challenge_method=S256`, 35 | redirectUri, 36 | scopes, 37 | codeVerifier, 38 | challenge, 39 | clientId: id, 40 | state, 41 | nonce, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/util/getCode.ts: -------------------------------------------------------------------------------- 1 | import { CookieAgent } from "http-cookie-agent/undici"; 2 | import { ok } from "node:assert"; 3 | import { URL, URLSearchParams } from "node:url"; 4 | import { CookieJar } from "tough-cookie"; 5 | import { interceptors, request } from "undici"; 6 | import type { Credentials } from "../types"; 7 | import { clientId } from "./Constants"; 8 | import { generateLoginLink } from "./generateLoginLink"; 9 | 10 | /** 11 | * Ottieni il codice per il login. 12 | * @param credentials - Le credenziali per l'accesso 13 | * @returns I dati del codice da usare 14 | */ 15 | export const getCode = async (credentials: Credentials) => { 16 | const link = await generateLoginLink(); 17 | const dispatcher = new CookieAgent({ 18 | allowH2: true, 19 | autoSelectFamily: true, 20 | autoSelectFamilyAttemptTimeout: 1, 21 | cookies: { jar: new CookieJar() }, 22 | }).compose( 23 | interceptors.retry(), 24 | interceptors.redirect({ maxRedirections: 3 }), 25 | ); 26 | const url = (await request(link.url, { dispatcher, maxRedirections: 0 })) 27 | .headers.location; 28 | 29 | ok(typeof url === "string", "Invalid login url"); 30 | const challenge = new URL(url).searchParams.get("login_challenge"); 31 | 32 | ok(challenge, "Invalid login challenge"); 33 | const { location } = await request( 34 | "https://www.portaleargo.it/auth/sso/login", 35 | { 36 | dispatcher, 37 | body: new URLSearchParams({ 38 | challenge, 39 | client_id: clientId, 40 | famiglia_customer_code: credentials.schoolCode, 41 | login: "true", 42 | password: credentials.password, 43 | username: credentials.username, 44 | }).toString(), 45 | headers: { "content-type": "application/x-www-form-urlencoded" }, 46 | method: "POST", 47 | }, 48 | ).then((r) => r.headers); 49 | 50 | ok(typeof location === "string", "Invalid login redirect"); 51 | const code = new URL(location).searchParams.get("code"); 52 | 53 | ok(code, "Invalid login code"); 54 | return { ...link, code }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/util/getToken.ts: -------------------------------------------------------------------------------- 1 | import type { APIToken, LoginLink } from "../types"; 2 | import { clientId } from "./Constants"; 3 | 4 | export const getToken = async (code: LoginLink & { code: string }) => { 5 | const date = new Date(); 6 | const res = await fetch("https://auth.portaleargo.it/oauth2/token", { 7 | headers: { 8 | "content-type": "application/x-www-form-urlencoded", 9 | }, 10 | body: new URLSearchParams({ 11 | code: code.code, 12 | grant_type: "authorization_code", 13 | redirect_uri: "it.argosoft.didup.famiglia.new://login-callback", 14 | code_verifier: code.codeVerifier, 15 | client_id: clientId, 16 | }).toString(), 17 | method: "POST", 18 | }); 19 | const data: APIToken = await res.json(); 20 | const expireDate = new Date(res.headers.get("date") ?? date); 21 | 22 | if ("error" in data) 23 | throw new Error(`${data.error} ${data.error_description}`); 24 | expireDate.setSeconds(expireDate.getSeconds() + data.expires_in); 25 | return Object.assign(data, { expireDate }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/util/handleOperation.ts: -------------------------------------------------------------------------------- 1 | import type { APIOperation } from "../types"; 2 | 3 | /** 4 | * Gestisci dei dati dell'API contenenti un'operazione. 5 | * @param array - L'array ricevuto 6 | * @param old - L'eventuale array da modificare 7 | * @param pk - Una funzione per estrarre il pk 8 | * @returns Il nuovo array 9 | */ 10 | export const handleOperation = ( 11 | array: APIOperation[], 12 | old: Omit< 13 | Extract, { operazione?: "I" }>, 14 | "operazione" 15 | >[] = [], 16 | ...[pk]: P extends true 17 | ? [ 18 | pk: ( 19 | a: Omit< 20 | Extract, { operazione?: "I" }>, 21 | "operazione" 22 | >, 23 | ) => string, 24 | ] 25 | : [] 26 | ) => { 27 | const toDelete: string[] = []; 28 | const getPk = 29 | (pk as 30 | | (( 31 | a: Omit< 32 | Extract, { operazione?: "I" }>, 33 | "operazione" 34 | >, 35 | ) => string) 36 | | undefined) ?? ((a) => a.pk!); 37 | 38 | for (const a of array) 39 | if (a.operazione === "D") toDelete.push(a.pk); 40 | else { 41 | const { operazione, ...rest } = a; 42 | const found = old.find((b) => a.pk === getPk(b)); 43 | 44 | if (found) Object.assign(found, rest); 45 | else old.push(rest); 46 | } 47 | return old.filter((a) => { 48 | const p = getPk(a); 49 | 50 | toDelete.unshift(p); 51 | return !toDelete.includes(p, 1); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /src/util/importData.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises"; 2 | import { join } from "node:path"; 3 | import type { ReadData } from "../types"; 4 | 5 | /** 6 | * Importa dei dati salvati in un file. 7 | * @param name - Il nome del file, escludendo l'estensione 8 | * @returns I dati importati 9 | */ 10 | export const importData = async ( 11 | name: T, 12 | path: string, 13 | ) => { 14 | try { 15 | return JSON.parse( 16 | await readFile(join(path, `${name}.json`), { 17 | encoding: "utf8", 18 | }), 19 | ) as ReadData[T]; 20 | } catch { 21 | return undefined; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Constants"; 2 | export * from "./encryptCodeVerifier"; 3 | export * from "./formatDate"; 4 | export * from "./generateLoginLink"; 5 | export * from "./getToken"; 6 | export * from "./handleOperation"; 7 | export * from "./randomString"; 8 | -------------------------------------------------------------------------------- /src/util/randomString.ts: -------------------------------------------------------------------------------- 1 | const characters = 2 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 3 | 4 | /** 5 | * Genera una stringa casuale. 6 | * @param length - La lunghezza della stringa 7 | * @returns La stringa generata 8 | */ 9 | export const randomString = (length: number) => { 10 | let result = ""; 11 | 12 | for (let i = 0; i < length; i++) 13 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 14 | return result; 15 | }; 16 | -------------------------------------------------------------------------------- /src/util/writeToFile.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from "node:fs/promises"; 2 | import { join } from "node:path"; 3 | 4 | /** 5 | * Salva dei dati in un file JSON. 6 | * @param name - Il nome del file, escludendo l'estensione 7 | * @param value - I dati da scrivere 8 | */ 9 | export const writeToFile = (name: string, value: unknown, path: string) => 10 | writeFile(`${join(path, name)}.json`, JSON.stringify(value)).catch( 11 | console.error, 12 | ); 13 | -------------------------------------------------------------------------------- /src/web.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./util"; 3 | export * from "./WebClient"; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "extends": [ 4 | "@tsconfig/recommended/tsconfig.json", 5 | "@tsconfig/strictest/tsconfig.json" 6 | ], 7 | "compilerOptions": { 8 | "allowSyntheticDefaultImports": true, 9 | "alwaysStrict": true, 10 | "declaration": true, 11 | "declarationDir": "./dist", 12 | "esModuleInterop": true, 13 | "exactOptionalPropertyTypes": false, 14 | "lib": ["es2020", "dom"], 15 | "module": "ES2022", 16 | "target": "ES2020", 17 | "moduleResolution": "bundler", 18 | "noImplicitAny": true, 19 | "noImplicitThis": true, 20 | "noPropertyAccessFromIndexSignature": false, 21 | "outDir": "./dist", 22 | "emitDeclarationOnly": true, 23 | "pretty": true, 24 | "resolveJsonModule": true, 25 | "rootDir": "./src", 26 | "sourceMap": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { env } from "process"; 3 | import { defineConfig, type Options } from "tsup"; 4 | 5 | const baseConfig: Options = { 6 | clean: true, 7 | format: "esm", 8 | keepNames: true, 9 | minify: env.NODE_ENV === "production", 10 | removeNodeProtocol: false, 11 | replaceNodeEnv: true, 12 | skipNodeModulesBundle: true, 13 | sourcemap: true, 14 | }; 15 | const options: Options[] = [ 16 | { 17 | entry: ["src/index.ts"], 18 | platform: "node", 19 | target: "node18", 20 | ...baseConfig, 21 | }, 22 | { 23 | entry: ["src/web.ts"], 24 | platform: "browser", 25 | target: "es2020", 26 | external: [/^node:/], 27 | ...baseConfig, 28 | }, 29 | ]; 30 | 31 | if (env.NODE_ENV !== "production") 32 | options.push({ 33 | entry: ["src/_test.ts"], 34 | platform: "node", 35 | target: "node22", 36 | ...baseConfig, 37 | }); 38 | 39 | export default defineConfig(options); 40 | --------------------------------------------------------------------------------