Skip to content
Snippets Groups Projects
Commit 642508e9 authored by Chiquita Ahsanunnisa's avatar Chiquita Ahsanunnisa
Browse files

Merge branch 'development' into 'main'

final release

See merge request !58
parents 2ad29b3d acb693b6
Branches
1 merge request!58final release
Pipeline #65989 failed with stages
Showing
with 1607 additions and 151 deletions
......@@ -2,4 +2,8 @@ POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DATABASE=
\ No newline at end of file
POSTGRES_DATABASE=
AUTH_SERVICE_URL=http://localhost:3001
FE_URL=http://localhost:5173
COOKIE_NAME=gradu-it.access-token
# Story/Task
Tulis jenisnya (hapus salah 1 di atas) apakah story/task dan tulis ini berkaitan dengan sprint backlog yang mana.<br>
[nomor sprint backlog (sesuai masing-masing kelompok)] - [Task]
# Details
Isi dengan detail apa aja yang kalian kerjain (fitur, perubahan, dll.).<br>
# Important Checks
Ceklis kalo kalian mengubah/menambahkan:
- [ ] Menambahkan `env` baru
- [ ] Mengubah skema basis data
# Endpoints
Isi dengan method (GET/POST/PUT/...), endpoint, dan keterangannya juga (misal: untuk menambah ... baru).<br>
| Method | Endpoint | Keterangan |
| -------- | ------------------------------------------------------ | ------------------- |
| | [/path/path](http://localhost:3000/[path/path]) | |
| | [/path/path](http://localhost:3000/[path/path]) | |
| | [/path/path](http://localhost:3000/[path/path]) | |
# Proof
Isi dengan screenshot dokumentasi API kalian. DOKUMENTASI HARUS DALAM BENTUK YANG DAPAT DIAKSES PADA `/api-docs`, TIDAK BOLEH MENGGUNAKAN POSTMAN/SWAGGER SENDIRI!<br>
| Method | Endpoint | Proof |
| -------- | ------------------------------------------------------ | ------------------- |
| | [/path/path](http://localhost:3000/[path/path]) | SCREENSHOT |
| | [/path/path](http://localhost:3000/[path/path]) | SCREENSHOT |
| | [/path/path](http://localhost:3000/[path/path]) | SCREENSHOT |
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
# PPL
## Running the app
## Git branching
```bash
# development
$ npm run start
Repository akan memiliki 2 branch utama, yaitu main dan development.
Setiap pembuatan branch baru, buat branch baru dengan base development.
Format: `<tipe>/<judul>`
# watch mode
$ npm run start:dev
List tipe:
# production mode
$ npm run start:prod
```
- Story, untuk fitur atau use case baru
- Task, untuk bug fixing, performance improvement, refactor, dsb.
Judul: gunakan kebab case
Contoh:
- story/api-attendance
- story/page-attendance
- task/improve-sql-performance-on-xxxx-method
## Test
Setelah selesai, Merge Request ke development dan wajib minta review ke scrum master.
```bash
# unit tests
$ npm run test
## Code Styling & Repository
# e2e tests
$ npm run test:e2e
Sangat dimohon untuk memperhatikan hal-hal berikut:
1. Penamaan variabel, fungsi, dan kelas yang bermakna.
2. Penyingkatan harus mudah ditebak dan masih terbaca.
- Misalkan, codeStylingAndRepository, terlalu panjang, disingkat menjadi: codeStyleNRepo.
- Yang Salah: csnr, cdStNrep.
3. Membuat kelas, type, dan interface dengan pascal case (ClassName).
4. Membuat fungsi dan variable dengan camel case (fungsiDanVariabel).
5. Membuat folder dan file dengan kebab case (nama-folder).
## Folder
```
src
├ entities
├ helper
├ middlewares
└ <nama-modul>
├ <nama-modul>.controller.ts
├ <nama-modul>.module.ts
├ <nama-modul>.dto.ts
└ <nama-modul>.service.ts
# test coverage
$ npm run test:cov
```
## Support
Folder menggunakan sistem modul NestJS yang bisa dilihat di https://docs.nestjs.com/modules.
Berikut merupakan penjelasan dasar dari setiap folder.
- `src/entities`
a. Berisi entity typeORM sesuai ERD yang ada di https://app.eraser.io/workspace/z0dwTFLk5F4reT6CYK7E.
b. Atribut entity (ex: title, description) bebas ditambahkan. Jika ada atribut yang diubah
atau dihapus, infokan ke yang lain karena mungkin berpengaruh ke pengerjaan sebelumnya.
c. Jika ingin menambahkan tabel atau relasi, diskusikan dengan yang lain.
d. Jika mengubah atribut atau tabel, update ERD agar sesuai.
- `src/helper`
a. Berisi fungsi utility atau helper.
- `src/middlewares`
a. Berisi midddleware aplikasi, bisa berupa guard atau interceptor.
- `src/<nama-modul>/.module.ts`
a. Berisi konfigurasi dasar dari sebuah modul.
- `src/<nama-modul>/.controller.ts`
a. Berisi controller yang akan melakukan mapping antara endpoint dengan handler-nya.
- `src/<nama-modul>/.service.ts`
a. Berisi service yang akan menerima request dan menghasilkan response.
- `src/<nama-modul>/.dto.ts`
a. Berisi data transfer object yang mendefinisikan struktur request ataupun response.
## Semantic Commit Message
- `feat`: (new feature for the user, not a new feature for build script)
- `fix`: (bug fix for the user, not a fix to a build script)
- `docs`: (changes to the documentation)
- `style`: (formatting, missing semi colons, etc; no production code change)
- `refactor`: (refactoring production code, eg. renaming a variable)
- `test`: (adding missing tests, refactoring tests; no production code change)
- `chore`: (updating grunt tasks etc; no production code change)
## Local Development Setup
### Git
Authorize ke github menggunakan SSH/ HTTPs. Referensi untuk SSH:
https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent
https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account
### Requirements
1. Node versi 21
### Instalasi Requirements
1. Install node 21 melalui node version manager. Referensi: https://github.com/nvm-sh/nvm#installing-and-updating
`nvm install lts/hydrogen`
### Langkah-Langkah
1. Clone repo `git clone git@gitlab.informatika.org:k-02-02/ppl-backend.git` atau `git clone https://gitlab.informatika.org/k-02-02/ppl-backend.git`
2. Install dependencies `npm install`
3. Sesuaikan env dengan file .env.example
4. Jalankan local dev derver `npm run start:dev`
## Techniques
### Schema Validation
Lakukan schema validation untuk **data yang masuk dari luar saat runtime (request body, params, dll)**. Tulis validasi di kelas DTO (buat kelas yang pendek boleh langsung pipe di controller). Dokumentasi:
- [NestJS Validation](https://docs.nestjs.com/techniques/validation)
- [Class Validator](https://www.npmjs.com/package/@nestjs/class-validator/v/0.13.1)
> **NOTE** <br> Schema validation bersifat whitelist, artinya kalo ga kalian pasang validasinya gak bakal bisa diakses meskipun di runtime kalian tambahin.
### API Documentation
Dokumentasi API bisa diakses di [http://localhost:3000/api-docs](http://localhost:3000/api-docs). Yang esensial:
| Decorator | Fungsi | Scope |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| `@ApiTags("nama-controller")` | Folder/grup API | Method / Controller |
| `@ApiOperation({ summary: "summary" })` | Description | Method |
| `@ApiResponse({ status: XXX, description: "desc", type: Type })` | Keterangan response API | Method / Controller |
| `@ApiBody({ type: Type })` | Enforce body secara hardcode. Kalo bisa jangan pake ini karena harusnya autogenerate dari `@Body()`. Pake ini kalo kalian pake middleware yang ngepass bodynya ke middleware bukan ke handler | Method |
| `@ApiCookieAuth()` | Auth pake cookie | Method / Controller |
| `@ApiProperty({ example: "example", description: "desc" })` | Register property kelas | Model |
| `@ApiHideProperty()` | Hide property kelas | Model |
Langkahnya kurang lebih:
1. Kalo buat tag baru, register tag nya di `src/main.ts` pake `.addTag("tag")`
2. Di kelas yang jadi model transfer object (entity / dto), kasih decorator property
3. Di bagian controller, kasih decorator sesuai kebutuhan auth, response, summary, dll
Dokumentasi:
- [NestJS OpenAPI](https://docs.nestjs.com/openapi/introduction)
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
### Environment Variables
## Stay in touch
Kalo nambahin environment variable, kalian harus:
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
- Tambahin di `env.example`
- Tambahin schema validation. Tulis validasi di `env.validation.ts`. Dokumentasi: [Class Validator](https://www.npmjs.com/package/@nestjs/class-validator/v/0.13.1)
## License
WARNING:
Nest is [MIT licensed](LICENSE).
- Sampe sekarang, `allowUnknown` masih di-set jadi `true`. Artinya kalian bisa aja masukin environment variable tanpa ngelakuin validasi skema. Masalahnya adalah kalo di-set ke `false`, environment variables bawaan lokal kalian kayak `USER`, `NODE_ENV` gitu-gitu jadi ke-restrict. Jadi tolong banget, pake environment variables yang emang udah ke-define aja di validasinya.
- Semua yang diakses pake `process.env.` masih string ya valuenya, dia gak auto transform (meskipun udah pake `class-transformer`). Jadi konversi sesuai kebutuhan masing-masing, ini bener-bener cuman buat validasi skemanya aja.
......@@ -10,11 +10,21 @@
"license": "UNLICENSED",
"dependencies": {
"@nestjs/axios": "^3.0.2",
"@nestjs/class-validator": "^0.13.4",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.1",
"@nestjs/typeorm": "^10.0.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cookie-parser": "^1.4.6",
"dayjs": "^1.11.10",
"passport": "^0.7.0",
"passport-custom": "^1.1.1",
"pg": "^8.11.3",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
......@@ -24,12 +34,15 @@
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/passport": "^1.0.16",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"axios": "^1.6.7",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
......@@ -1670,6 +1683,11 @@
"node": ">=8"
}
},
"node_modules/@microsoft/tsdoc": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz",
"integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug=="
},
"node_modules/@nestjs/axios": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz",
......@@ -1680,6 +1698,15 @@
"rxjs": "^6.0.0 || ^7.0.0"
}
},
"node_modules/@nestjs/class-validator": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/@nestjs/class-validator/-/class-validator-0.13.4.tgz",
"integrity": "sha512-/mqZL36LJ5uV5WDhi87Cd52IssuO+SStaOr2+6sBsvCCGUWkoJes4Wwzmm3m/gdHH+tsNxX60sVSzYcU6hAy9Q==",
"dependencies": {
"libphonenumber-js": "^1.9.43",
"validator": "^13.7.0"
}
},
"node_modules/@nestjs/cli": {
"version": "10.3.2",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz",
......@@ -1888,14 +1915,42 @@
}
}
},
"node_modules/@nestjs/mapped-types": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
"integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==",
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
"class-transformer": "^0.4.0 || ^0.5.0",
"class-validator": "^0.13.0 || ^0.14.0",
"reflect-metadata": "^0.1.12 || ^0.2.0"
},
"peerDependenciesMeta": {
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/passport": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.3.tgz",
"integrity": "sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==",
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
"passport": "^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0"
}
},
"node_modules/@nestjs/platform-express": {
"version": "10.3.3",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.3.tgz",
"integrity": "sha512-GGKSEU48Os7nYFIsUM0nutuFUGn5AbeP8gzFBiBCAtiuJWrXZXpZ58pMBYxAbMf7IrcOZFInHEukjHGAQU0OZw==",
"version": "10.3.7",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz",
"integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==",
"dependencies": {
"body-parser": "1.20.2",
"cors": "2.8.5",
"express": "4.18.2",
"express": "4.19.2",
"multer": "1.4.4-lts.1",
"tslib": "2.6.2"
},
......@@ -1930,6 +1985,38 @@
"integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true
},
"node_modules/@nestjs/swagger": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.1.tgz",
"integrity": "sha512-LUC4mr+5oAleEC/a2j8pNRh1S5xhKXJ1Gal5ZdRjt9XebQgbngXCdW7JTA9WOEcwGtFZN9EnKYdquzH971LZfw==",
"dependencies": {
"@microsoft/tsdoc": "^0.14.2",
"@nestjs/mapped-types": "2.0.5",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"path-to-regexp": "3.2.0",
"swagger-ui-dist": "5.11.2"
},
"peerDependencies": {
"@fastify/static": "^6.0.0 || ^7.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0",
"class-transformer": "*",
"class-validator": "*",
"reflect-metadata": "^0.1.12 || ^0.2.0"
},
"peerDependenciesMeta": {
"@fastify/static": {
"optional": true
},
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/testing": {
"version": "10.3.3",
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.3.tgz",
......@@ -2158,6 +2245,15 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/cookiejar": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
......@@ -2290,6 +2386,15 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/passport": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
"integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.11",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
......@@ -2356,6 +2461,11 @@
"@types/superagent": "^8.1.0"
}
},
"node_modules/@types/validator": {
"version": "13.11.9",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
"integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw=="
},
"node_modules/@types/yargs": {
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
......@@ -2919,8 +3029,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/array-flatten": {
"version": "1.1.1",
......@@ -2957,7 +3066,6 @@
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
......@@ -3443,6 +3551,21 @@
"integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==",
"dev": true
},
"node_modules/class-transformer": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="
},
"node_modules/class-validator": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
"integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==",
"dependencies": {
"@types/validator": "^13.11.8",
"libphonenumber-js": "^1.10.53",
"validator": "^13.9.0"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
......@@ -3728,9 +3851,29 @@
"dev": true
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"dependencies": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
......@@ -4450,16 +4593,16 @@
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
......@@ -4490,29 +4633,6 @@
"node": ">= 0.10.0"
}
},
"node_modules/express/node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
......@@ -4531,20 +4651,6 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/express/node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
......@@ -4794,16 +4900,15 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"peer": true,
"engines": {
"node": ">=4.0"
},
......@@ -6296,7 +6401,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
......@@ -6410,6 +6514,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/libphonenumber-js": {
"version": "1.10.58",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.58.tgz",
"integrity": "sha512-53A0IpJFL9LdHbpeatwizf8KSwPICrqn9H0g3Y7WQ+Jgeu9cQ4Ew3WrRtrLBu/CX2lXd5+rgT01/tGlkbkzOjw=="
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
......@@ -7002,6 +7111,42 @@
"node": ">= 0.8"
}
},
"node_modules/passport": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-custom": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz",
"integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==",
"dependencies": {
"passport-strategy": "1.x.x"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
......@@ -7071,6 +7216,11 @@
"node": ">=8"
}
},
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/pg": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
......@@ -7384,8 +7534,7 @@
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"peer": true
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.1",
......@@ -8332,6 +8481,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz",
"integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A=="
},
"node_modules/symbol-observable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
......@@ -9084,6 +9238,14 @@
"node": ">=10.12.0"
}
},
"node_modules/validator": {
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
"integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
......
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
NotFoundException,
Param,
Post,
Put,
Query,
Req,
UseGuards,
} from "@nestjs/common";
import {
ApiBearerAuth,
ApiCookieAuth,
ApiCreatedResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
} from "@nestjs/swagger";
import { RoleEnum } from "src/entities/pengguna.entity";
import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
import { Roles } from "src/middlewares/roles.decorator";
import { RolesGuard } from "src/middlewares/roles.guard";
import {
CreateBulkTopikDto,
TopikIdRespDto,
CreateTopikDto,
GetAllRespDto,
OmittedTopik,
TopikParamDto,
TopikQueryDto,
UpdateTopikDto,
createBulkRespDto,
} from "./alokasi-topik.dto";
import { AlokasiTopikService } from "./alokasi-topik.service";
import { Request } from "express";
import { AuthDto } from "src/auth/auth.dto";
import { HIGH_AUTHORITY_ROLES, isHighAuthority } from "src/helper/roles";
@ApiTags("Alokasi Topik")
@ApiCookieAuth()
@ApiBearerAuth()
@Controller("alokasi-topik")
@UseGuards(CustomAuthGuard, RolesGuard)
export class AlokasiTopikController {
constructor(private alokasiTopikService: AlokasiTopikService) {}
@ApiOperation({
summary: "Create new topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING",
})
@ApiCreatedResponse({ type: TopikIdRespDto })
@Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING)
@Post()
async create(
@Body() createDto: CreateTopikDto,
@Req() req: Request,
): Promise<TopikIdRespDto> {
const { roles, id } = req.user as AuthDto;
// user only has S2_PEMBIMBING role
if (!isHighAuthority(roles) && createDto.idPengaju !== id) {
throw new BadRequestException("Pengaju ID harus sama dengan user ID");
}
return await this.alokasiTopikService.create(createDto);
}
@ApiOperation({
summary: "Create multiple topik. Roles: S2_TIM_TESIS, ADMIN",
})
@ApiOkResponse({ type: createBulkRespDto })
@Roles(...HIGH_AUTHORITY_ROLES)
@Post("/bulk")
async createBulk(@Body() createDto: CreateBulkTopikDto) {
return await this.alokasiTopikService.createBulk(createDto);
}
@ApiOperation({
summary:
"Get topik by ID. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING, S2_MAHASISWA",
})
@ApiOkResponse({ type: OmittedTopik })
@Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING, RoleEnum.S2_MAHASISWA)
@Get("/:id")
async getById(@Param() params: TopikParamDto) {
const res = await this.alokasiTopikService.findActiveTopikById(params.id);
if (!res) throw new NotFoundException();
return res as OmittedTopik;
}
@ApiOperation({
summary:
"Get all topik. Roles: S2_TIM_TESIS, ADMIN, S2_MAHASISWA, S2_PEMBIMBING",
})
@ApiOkResponse({ type: GetAllRespDto })
@Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_MAHASISWA, RoleEnum.S2_PEMBIMBING)
@Get()
async getAll(
@Query()
query: TopikQueryDto,
) {
return await this.alokasiTopikService.findAllActiveTopikCreatedByPembimbing(
{
...query,
page: query.page || 1,
},
);
}
@ApiOperation({
summary: "Update topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING",
})
@ApiOkResponse({ type: TopikIdRespDto })
@Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING)
@Put("/:id")
async update(
@Param() params: TopikParamDto,
@Body() updateDto: UpdateTopikDto,
@Req() req: Request,
): Promise<TopikIdRespDto> {
let idPengaju = undefined;
const { roles, id } = req.user as AuthDto;
// user only has S2_PEMBIMBING role
if (!isHighAuthority(roles)) {
if (updateDto.idPengaju !== id) {
throw new BadRequestException("Pengaju ID harus sama dengan user ID");
}
idPengaju = id;
}
const res = await this.alokasiTopikService.update(
params.id,
updateDto,
idPengaju,
);
if (!res.affected)
throw new NotFoundException(
"Topik tidak ditemukan di antara topik yang dapat Anda akses",
);
const resp: TopikIdRespDto = { id: params.id };
return resp;
}
@ApiOperation({
summary: "Delete topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING",
})
@ApiOkResponse({ type: TopikIdRespDto })
@Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING)
@Delete("/:id")
async delete(
@Param() params: TopikParamDto,
@Req() req: Request,
): Promise<TopikIdRespDto> {
let idPengaju = undefined;
const { roles, id } = req.user as AuthDto;
// user only has S2_PEMBIMBING role
if (!isHighAuthority(roles)) {
idPengaju = id;
}
const res = await this.alokasiTopikService.remove(params.id, idPengaju);
if (!res.affected)
throw new NotFoundException(
"Topik tidak ditemukan di antara topik yang dapat Anda akses",
);
const resp: TopikIdRespDto = { id: params.id };
return resp;
}
}
import {
IsNumberString,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from "@nestjs/class-validator";
import { ApiProperty, ApiPropertyOptional, OmitType } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { Pengguna } from "src/entities/pengguna.entity";
import { Topik } from "src/entities/topik.entity";
export class CreateTopikDto {
@ApiProperty()
@IsString()
judul: string;
@ApiProperty()
@IsString()
deskripsi: string;
@ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
@IsUUID()
idPengaju: string;
}
export class CreateBulkTopikDto {
@ApiProperty({ type: [CreateTopikDto] })
@ValidateNested({ each: true })
@Type(() => CreateTopikDto)
data: CreateTopikDto[];
}
export class UpdateTopikDto extends CreateTopikDto {}
export class TopikParamDto {
@ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
@IsUUID()
id: string;
}
export class TopikQueryDto {
@IsOptional()
@IsNumberString()
@ApiPropertyOptional({ description: "default: 1" })
page?: number;
@IsOptional()
@IsNumberString()
@ApiPropertyOptional({ description: "default: no limit" })
limit?: number;
@IsOptional()
@IsString()
@ApiPropertyOptional()
search?: string;
@IsOptional()
@IsUUID()
@ApiPropertyOptional({ example: "550e8400-e29b-41d4-a716-446655440000" })
idPembimbing?: string;
}
export class PengajuDto extends OmitType(Pengguna, ["nim", "aktif"] as const) {}
export class OmittedTopik extends OmitType(Topik, ["idPengaju"] as const) {
@ApiProperty({ type: PengajuDto })
pengaju: Pengguna;
}
export class GetAllRespDto {
@ApiProperty({ type: [OmittedTopik] })
data: OmittedTopik[];
@ApiProperty()
maxPage: number;
}
export class createBulkRespDto {
@ApiProperty()
ids: string[];
}
export class TopikIdRespDto {
@ApiProperty()
id: string;
}
import { Module } from "@nestjs/common";
import { AlokasiTopikService } from "./alokasi-topik.service";
import { AlokasiTopikController } from "./alokasi-topik.controller";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Topik } from "src/entities/topik.entity";
import { CustomStrategy } from "src/middlewares/custom.strategy";
import { AuthModule } from "src/auth/auth.module";
@Module({
imports: [TypeOrmModule.forFeature([Topik]), AuthModule],
providers: [AlokasiTopikService, CustomStrategy],
controllers: [AlokasiTopikController],
})
export class AlokasiTopikModule {}
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { RoleEnum } from "src/entities/pengguna.entity";
import { Topik } from "src/entities/topik.entity";
import { ArrayContains, ILike, Repository } from "typeorm";
import {
CreateBulkTopikDto,
TopikIdRespDto,
CreateTopikDto,
GetAllRespDto,
UpdateTopikDto,
createBulkRespDto,
} from "./alokasi-topik.dto";
@Injectable()
export class AlokasiTopikService {
constructor(@InjectRepository(Topik) private topikRepo: Repository<Topik>) {}
async create(createDto: CreateTopikDto): Promise<TopikIdRespDto> {
const ids = (await this.topikRepo.insert(createDto)).identifiers;
return { id: ids[0].id };
}
async createBulk(createDto: CreateBulkTopikDto): Promise<createBulkRespDto> {
const ids = (
await this.topikRepo.insert(createDto.data.map((dto) => ({ ...dto })))
).identifiers;
return { ids: ids.map(({ id }) => id) };
}
async findActiveTopikById(id: string) {
return await this.topikRepo.findOne({
select: {
id: true,
judul: true,
deskripsi: true,
pengaju: {
id: true,
nama: true,
email: true,
roles: true,
},
},
where: {
id,
aktif: true,
},
relations: {
pengaju: true,
},
});
}
async findAllActiveTopikCreatedByPembimbing(options: {
page: number;
limit?: number;
search?: string;
idPembimbing?: string;
}): Promise<GetAllRespDto> {
const dataQuery = this.topikRepo.find({
select: {
id: true,
judul: true,
deskripsi: true,
pengaju: {
id: true,
nama: true,
email: true,
roles: true,
},
},
where: {
aktif: true,
pengaju: {
id: options.idPembimbing || undefined,
roles: ArrayContains([RoleEnum.S2_PEMBIMBING]),
},
judul: ILike(`%${options.search || ""}%`),
},
relations: {
pengaju: true,
},
order: {
pengaju: {
nama: "ASC",
},
judul: "ASC",
},
take: options.limit || undefined,
skip: options.limit ? (options.page - 1) * options.limit : 0,
});
if (options.limit) {
let countQuery = this.topikRepo
.createQueryBuilder("topik")
.select("topik.id")
.innerJoinAndSelect("topik.pengaju", "pengaju")
.where("pengaju.roles @> :role", {
role: [RoleEnum.S2_PEMBIMBING],
});
if (options.idPembimbing) {
countQuery = countQuery.andWhere("pengaju.id = :id", {
id: options.idPembimbing || undefined,
});
}
if (options.search) {
countQuery = countQuery.andWhere("topik.judul LIKE :search", {
search: `%${options.search || ""}%`,
});
}
const [count, data] = await Promise.all([
countQuery.getCount(),
dataQuery,
]);
return {
maxPage: Math.ceil(count / options.limit),
data,
};
} else {
const data = await dataQuery;
return {
maxPage: data.length ? 1 : 0,
data,
};
}
}
async update(id: string, updateDto: UpdateTopikDto, idPengaju?: string) {
const findOpt = idPengaju
? { id, idPengaju, aktif: true }
: { id, aktif: true };
return await this.topikRepo.update(findOpt, updateDto);
}
async remove(id: string, idPengaju?: string) {
const findOpt = idPengaju ? { id, idPengaju } : { id };
return await this.topikRepo.update(findOpt, { aktif: false });
}
}
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Bimbingan } from "./entities/bimbingan.entity";
import { Pengguna } from "./entities/pengguna.entity";
import { RangeJadwalSeminar } from "./entities/rangeJadwalSeminar.entity";
import { Seminar } from "./entities/seminar.entity";
import { Topik } from "./entities/topik.entity";
import { AuditLog } from "./entities/auditLog.entity";
import { DosenBimbingan } from "./entities/dosenBimbingan.entity";
import { Kelas } from "./entities/kelas.entity";
import { MahasiswaKelas } from "./entities/mahasiswaKelas";
import { PengajarKelas } from "./entities/pengajarKelas.entity";
import { PengajuanPengambilanTopik } from "./entities/pengajuanPengambilanTopik.entity";
import { PengambilanTopik } from "./entities/pengambilanTopik.entity";
import { RangeJadwalSidang } from "./entities/rangeJadwalSidang.entity";
import { Ruangan } from "./entities/ruangan.entity";
import { Sidang } from "./entities/sidang.entity";
import { Tugas } from "./entities/tugas.entity";
import { PembimbingSeminar } from "./entities/pembimbingSeminar.entity";
import { PembimbingSidang } from "./entities/pembimbingSidang.entity";
import { PengujiSidang } from "./entities/pengujiSidang.entity";
import { PendaftaranTesis } from "./entities/pendaftaranTesis.entity";
import { PengujiSidsem } from "./entities/pengujiSidsem.entity";
import { RegistrasiTesisModule } from "./registrasi-tesis/registrasi-tesis.module";
import { ConfigModule } from "@nestjs/config";
import { RegistrasiSidsemModule } from "./registrasi-sidsem/registrasi-sidsem.module";
import { AlokasiTopikModule } from "./alokasi-topik/alokasi-topik.module";
import { AuthModule } from "./auth/auth.module";
import { BimbinganModule } from "./bimbingan/bimbingan.module";
import { validate } from "./env.validation";
import { BerkasBimbingan } from "./entities/berkasBimbingan.entity";
import { PendaftaranSidsem } from "./entities/pendaftaranSidsem";
import { DosenBimbinganModule } from "./dosen-bimbingan/dosen-bimbingan.module";
import { PenggunaModule } from "./pengguna/pengguna.module";
import { KonfigurasiModule } from "./konfigurasi/konfigurasi.module";
import { Konfigurasi } from "./entities/konfigurasi.entity";
import { DashboardModule } from "./dashboard/dashboard.module";
import { BerkasSidsem } from "./entities/berkasSidsem.entity";
import { DosenPengujiModule } from "./dosen-penguji/dosen-penguji.module";
@Module({
imports: [
ConfigModule.forRoot(),
ConfigModule.forRoot({ validate }),
TypeOrmModule.forRoot({
type: "postgres",
host: process.env.POSTGRES_HOST,
......@@ -36,30 +37,29 @@ import { ConfigModule } from "@nestjs/config";
database: process.env.POSTGRES_DATABASE,
ssl: process.env.POSTGRES_HOST !== "localhost",
entities: [
BerkasBimbingan,
Bimbingan,
Pengguna,
RangeJadwalSeminar,
Seminar,
PendaftaranSidsem,
Topik,
AuditLog,
DosenBimbingan,
Kelas,
MahasiswaKelas,
PengajarKelas,
PengajuanPengambilanTopik,
PengambilanTopik,
RangeJadwalSidang,
Ruangan,
Sidang,
Tugas,
PembimbingSeminar,
PembimbingSidang,
PengujiSidang,
Konfigurasi,
PendaftaranTesis,
PengujiSidsem,
BerkasSidsem,
],
// autoLoadEntities: true,
synchronize: true,
}),
RegistrasiTesisModule,
AuthModule,
AlokasiTopikModule,
DashboardModule,
BimbinganModule,
DosenBimbinganModule,
RegistrasiSidsemModule,
PenggunaModule,
KonfigurasiModule,
DosenPengujiModule,
],
controllers: [AppController],
providers: [AppService],
......
import { RoleEnum } from "src/entities/pengguna.entity";
export class AuthDto {
id: string;
nama: string;
email: string;
roles: RoleEnum[];
}
import { Module } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { HttpModule } from "@nestjs/axios";
@Module({
imports: [HttpModule],
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
import { HttpService } from "@nestjs/axios";
import { HttpException, Injectable } from "@nestjs/common";
import { AxiosError } from "axios";
import { catchError, firstValueFrom } from "rxjs";
import { AuthDto } from "src/auth/auth.dto";
@Injectable()
export class AuthService {
constructor(private httpService: HttpService) {}
async validate(token: string) {
const user = await firstValueFrom(
this.httpService
.get(`${process.env.AUTH_SERVICE_URL}/auth/self`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
.pipe(
catchError((error: AxiosError) => {
throw new HttpException(error.response.data, error.response.status);
}),
),
);
return user.data as AuthDto;
}
}
import {
Body,
Controller,
Get,
Param,
Patch,
Post,
Req,
UseGuards,
} from "@nestjs/common";
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiBody,
ApiCookieAuth,
ApiForbiddenResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiResponse,
ApiTags,
} from "@nestjs/swagger";
import { Request } from "express";
import { AuthDto } from "src/auth/auth.dto";
import { RoleEnum } from "src/entities/pengguna.entity";
import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
import { Roles } from "src/middlewares/roles.decorator";
import { RolesGuard } from "src/middlewares/roles.guard";
import {
ByMhsIdDto,
CreateBimbinganReqDto,
CreateBimbinganResDto,
GetByBimbinganIdResDto,
GetByMahasiswaIdResDto,
UpdateStatusDto,
UpdateStatusResDto,
} from "./bimbingan.dto";
import { BimbinganService } from "./bimbingan.service";
@ApiTags("Bimbingan")
@ApiCookieAuth()
@ApiBearerAuth()
@Controller("bimbingan")
@UseGuards(CustomAuthGuard, RolesGuard)
export class BimbinganController {
constructor(private readonly bimbinganService: BimbinganService) {}
@ApiOkResponse({ type: GetByMahasiswaIdResDto })
@ApiNotFoundResponse({ description: "Tidak ada pendaftaran" })
@Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
@Get("/mahasiswa/:mahasiswaId")
async getByMahasiswaId(
@Param() param: ByMhsIdDto,
@Req() request: Request,
): Promise<GetByMahasiswaIdResDto> {
return (await this.bimbinganService.getByMahasiswaId(
param.mahasiswaId,
request.user as AuthDto,
)) as GetByMahasiswaIdResDto;
}
@ApiOkResponse({ type: GetByMahasiswaIdResDto })
@ApiNotFoundResponse({ description: "Tidak ada pendaftaran" })
@Roles(RoleEnum.S2_MAHASISWA)
@Get("/")
async getOwnBimbingan(
@Req() request: Request,
): Promise<GetByMahasiswaIdResDto> {
return this.bimbinganService.getByMahasiswaId(
(request.user as AuthDto).id,
request.user as AuthDto,
);
}
@ApiResponse({ status: 201, type: CreateBimbinganResDto })
@ApiBadRequestResponse({
description:
"Waktu bimbingan lebih dari hari ini atau bimbingan berikutnya sebelum waktu bimbingan yang dimasukkan",
})
@ApiNotFoundResponse({ description: "Tidak ada pendaftaran" })
@Roles(RoleEnum.S2_MAHASISWA)
@ApiBody({ type: CreateBimbinganReqDto })
@Post("/")
async createBimbinganLog(
@Req() request: Request,
@Body() body: CreateBimbinganReqDto,
): Promise<CreateBimbinganResDto> {
return this.bimbinganService.create((request.user as AuthDto).id, body);
}
@ApiOkResponse({ type: UpdateStatusResDto })
@ApiNotFoundResponse({ description: "Bimbingan tidak ditemukan" })
@ApiForbiddenResponse({
description: "Anda tidak memiliki akses untuk mengubah status bimbingan",
})
@Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN)
@ApiBody({ type: UpdateStatusDto })
@Patch("/pengesahan")
async updateStatus(
@Req() request: Request,
@Body() body: UpdateStatusDto,
): Promise<UpdateStatusResDto> {
return this.bimbinganService.updateStatus(request.user as AuthDto, body);
}
@ApiOkResponse({ type: GetByBimbinganIdResDto })
@ApiNotFoundResponse({ description: "Bimbingan tidak ditemukan" })
@Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN)
@Get("/:bimbinganId")
async getByBimbinganId(
@Req() request: Request,
@Param("bimbinganId") bimbinganId: string,
): Promise<GetByBimbinganIdResDto> {
return this.bimbinganService.getByBimbinganId(
request.user as AuthDto,
bimbinganId,
);
}
}
import {
IsBoolean,
IsDateString,
IsDefined,
IsOptional,
IsString,
IsUUID,
ValidateNested,
} from "@nestjs/class-validator";
import {
ApiProperty,
IntersectionType,
OmitType,
PickType,
} from "@nestjs/swagger";
import { Type } from "class-transformer";
import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
import { Bimbingan, BimbinganStatus } from "src/entities/bimbingan.entity";
import {
JalurEnum,
PendaftaranTesis,
} from "src/entities/pendaftaranTesis.entity";
import { Pengguna } from "src/entities/pengguna.entity";
import { Topik } from "src/entities/topik.entity";
class MhsRes extends PickType(Pengguna, ["id", "nama", "email"] as const) {
@ApiProperty({ enum: JalurEnum })
jalurPilihan: JalurEnum;
}
class PickedTopikBimbingan extends PickType(Topik, [
"id",
"judul",
"deskripsi",
"idPengaju",
] as const) {}
export class GetByMahasiswaIdResDto {
@ApiProperty({ type: [Bimbingan] })
bimbingan: Bimbingan[];
@ApiProperty({ type: MhsRes })
mahasiswa: MhsRes;
@ApiProperty({ type: PickedTopikBimbingan })
topik: PickedTopikBimbingan;
@ApiProperty({ enum: BimbinganStatus })
status: BimbinganStatus;
}
class BerkasWithoutId extends OmitType(BerkasBimbingan, ["id"] as const) {}
export class CreateBimbinganReqDto {
@ApiProperty({ type: Date })
@IsDateString()
waktuBimbingan: string;
@ApiProperty()
@IsString()
laporanKemajuan: string;
@ApiProperty()
@IsString()
todo: string;
@ApiProperty({ type: Date })
@IsDateString()
@IsOptional()
bimbinganBerikutnya: string;
@ApiProperty({ type: [BerkasWithoutId] })
@ValidateNested({ each: true })
@Type(() => BerkasWithoutId)
@IsDefined()
berkas: BerkasWithoutId[];
}
export class CreateBimbinganResDto {
@ApiProperty()
id: string;
}
export class ByMhsIdDto {
@ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
@IsUUID()
mahasiswaId: string;
}
export class UpdateStatusDto {
@ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
@IsUUID()
bimbinganId: string;
@ApiProperty()
@IsBoolean()
status: boolean;
}
export class UpdateStatusResDto {
@ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
id: string;
}
export class GetByBimbinganIdResDto extends IntersectionType(
OmitType(Bimbingan, ["pendaftaran"] as const),
PickType(PendaftaranTesis, ["jalurPilihan"] as const),
) {}
import { Module } from "@nestjs/common";
import { BimbinganController } from "./bimbingan.controller";
import { BimbinganService } from "./bimbingan.service";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Bimbingan } from "src/entities/bimbingan.entity";
import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity";
import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
import { PenggunaModule } from "src/pengguna/pengguna.module";
import { PenggunaService } from "src/pengguna/pengguna.service";
import { Pengguna } from "src/entities/pengguna.entity";
@Module({
imports: [
TypeOrmModule.forFeature([
Bimbingan,
PendaftaranTesis,
DosenBimbingan,
BerkasBimbingan,
Pengguna,
]),
PenggunaModule,
],
controllers: [BimbinganController],
providers: [BimbinganService, PenggunaService],
exports: [BimbinganService],
})
export class BimbinganModule {}
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import * as dayjs from "dayjs";
import { AuthDto } from "src/auth/auth.dto";
import { Bimbingan, BimbinganStatus } from "src/entities/bimbingan.entity";
import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
import {
PendaftaranTesis,
RegStatus,
} from "src/entities/pendaftaranTesis.entity";
import { RoleEnum } from "src/entities/pengguna.entity";
import { Repository } from "typeorm";
import {
CreateBimbinganReqDto,
CreateBimbinganResDto,
GetByBimbinganIdResDto,
GetByMahasiswaIdResDto,
UpdateStatusDto,
UpdateStatusResDto,
} from "./bimbingan.dto";
import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
import { PenggunaService } from "src/pengguna/pengguna.service";
@Injectable()
export class BimbinganService {
constructor(
@InjectRepository(Bimbingan)
private bimbinganRepository: Repository<Bimbingan>,
@InjectRepository(PendaftaranTesis)
private pendaftaranTesisRepository: Repository<PendaftaranTesis>,
@InjectRepository(DosenBimbingan)
private dosenBimbinganRepository: Repository<DosenBimbingan>,
@InjectRepository(BerkasBimbingan)
private berkasBimbinganRepository: Repository<BerkasBimbingan>,
private penggunaService: PenggunaService,
) {}
async getByMahasiswaId(
mahasiswaId: string,
user: AuthDto,
): Promise<GetByMahasiswaIdResDto> {
await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId);
const pendaftaran = await this.pendaftaranTesisRepository.findOne({
where: {
mahasiswa: { id: mahasiswaId },
status: RegStatus.APPROVED,
},
relations: {
mahasiswa: true,
topik: true,
penerima: true,
},
});
if (!pendaftaran) {
throw new NotFoundException("Tidak ada pendaftaran yang disetujui");
}
// Validate bimbingan data by its dosbim
// only validate if user isn't high authority
// and user is not mhs checking their own logs
if (
!user.roles.includes(RoleEnum.ADMIN) &&
!user.roles.includes(RoleEnum.S2_TIM_TESIS) &&
!(user.roles.includes(RoleEnum.S2_MAHASISWA) && mahasiswaId == user.id)
) {
const dosbim = await this.dosenBimbinganRepository.find({
where: { pendaftaran: { id: pendaftaran.id } },
relations: { dosen: true },
});
if (!dosbim.map((d) => d.dosen.id).includes(user.id)) {
throw new ForbiddenException();
}
}
const bimbingan = await this.bimbinganRepository.find({
where: {
pendaftaran: {
id: pendaftaran.id,
},
},
relations: {
berkas: true,
},
});
const status = await this.getBimbinganStatus(pendaftaran);
return {
bimbingan,
mahasiswa: {
id: pendaftaran.mahasiswa.id,
nama: pendaftaran.mahasiswa.nama,
email: pendaftaran.mahasiswa.email,
jalurPilihan: pendaftaran.jalurPilihan,
},
topik: pendaftaran.topik,
status,
};
}
async create(
mahasiswaId: string,
createDto: CreateBimbinganReqDto,
): Promise<CreateBimbinganResDto> {
// Check if user registered in bimbingan
const pendaftaran = await this.pendaftaranTesisRepository.findOne({
where: {
mahasiswa: { id: mahasiswaId },
status: RegStatus.APPROVED,
},
relations: {
mahasiswa: true,
topik: true,
penerima: true,
},
});
if (!pendaftaran) {
throw new NotFoundException("Tidak ada pendaftaran yang disetujui");
}
if (dayjs(createDto.waktuBimbingan).isAfter(dayjs(new Date()).endOf("d")))
throw new BadRequestException(
"Tanggal bimbingan yang dimasukkan tidak boleh melebihi tanggal hari ini",
);
if (
dayjs(createDto.bimbinganBerikutnya)
.endOf("D")
.isBefore(dayjs(createDto.waktuBimbingan).startOf("D"))
)
throw new BadRequestException(
"Bimbingan berikutnya harus setelah bimbingan yang dimasukkan",
);
const berkasBimbingan = createDto.berkas.map((berkas) =>
this.berkasBimbinganRepository.create(berkas),
);
const createdBimbinganLog = this.bimbinganRepository.create({
waktuBimbingan: createDto.waktuBimbingan,
laporanKemajuan: createDto.laporanKemajuan,
todo: createDto.todo,
bimbinganBerikutnya: createDto.bimbinganBerikutnya,
berkas: berkasBimbingan,
pendaftaran,
});
await this.bimbinganRepository.save(createdBimbinganLog);
return { id: createdBimbinganLog.id };
}
async updateStatus(
user: AuthDto,
dto: UpdateStatusDto,
): Promise<UpdateStatusResDto> {
const bimbingan = await this.getByBimbinganId(user, dto.bimbinganId); // already check if mahasiswa is aktif
await this.bimbinganRepository.update(bimbingan.id, {
disahkan: dto.status,
});
return {
id: bimbingan.id,
};
}
async getByBimbinganId(
user: AuthDto,
bimbinganId: string,
): Promise<GetByBimbinganIdResDto> {
const bimbinganQuery = this.bimbinganRepository
.createQueryBuilder("bimbingan")
.leftJoinAndSelect("bimbingan.pendaftaran", "pendaftaran")
.leftJoinAndSelect("pendaftaran.dosenBimbingan", "dosenBimbingan")
.leftJoinAndSelect("dosenBimbingan.dosen", "dosen")
.leftJoinAndSelect("bimbingan.berkas", "berkas")
.leftJoin("pendaftaran.topik", "topik")
.leftJoinAndSelect("pendaftaran.mahasiswa", "mahasiswa")
.where("bimbingan.id = :id", { id: bimbinganId });
const bimbingan = await bimbinganQuery.getOne();
if (!bimbingan) {
throw new NotFoundException("Bimbingan tidak ditemukan");
}
if (!bimbingan.pendaftaran.mahasiswa.aktif) {
throw new BadRequestException("Bimbingan milik mahasiswa tidak aktif");
}
if (
!user.roles.includes(RoleEnum.ADMIN) &&
!bimbingan.pendaftaran.dosenBimbingan
.map((d) => d.dosen.id)
.includes(user.id)
) {
throw new ForbiddenException();
}
return {
id: bimbingan.id,
waktuBimbingan: bimbingan.waktuBimbingan,
laporanKemajuan: bimbingan.laporanKemajuan,
todo: bimbingan.todo,
bimbinganBerikutnya: bimbingan.bimbinganBerikutnya,
disahkan: bimbingan.disahkan,
berkas: bimbingan.berkas,
jalurPilihan: bimbingan.pendaftaran.jalurPilihan,
};
}
async getBimbinganStatus(
pendaftaran: PendaftaranTesis,
): Promise<BimbinganStatus> {
const lastBimbinganQuery = this.bimbinganRepository
.createQueryBuilder("bimbingan")
.where("bimbingan.pendaftaranId = :pendaftaranId", {
pendaftaranId: pendaftaran.id,
})
.orderBy("bimbingan.waktuBimbingan", "DESC")
.limit(1);
const lastBimbingan = await lastBimbinganQuery.getOne();
if (!lastBimbingan) {
return dayjs(pendaftaran.waktuPengiriman).isBefore(
dayjs().subtract(3, "month"),
)
? BimbinganStatus.TERKENDALA
: BimbinganStatus.LANCAR;
}
if (
dayjs(lastBimbingan.waktuBimbingan).isBefore(dayjs().subtract(3, "month"))
) {
return BimbinganStatus.TERKENDALA;
} else if (
dayjs(lastBimbingan.waktuBimbingan).isBefore(dayjs().subtract(1, "month"))
) {
return BimbinganStatus.BUTUH_BIMBINGAN;
} else {
return BimbinganStatus.LANCAR;
}
}
}
import { Controller, Get, Query, Req, UseGuards } from "@nestjs/common";
import {
ApiBearerAuth,
ApiCookieAuth,
ApiOkResponse,
ApiTags,
} from "@nestjs/swagger";
import { Request } from "express";
import { AuthDto } from "src/auth/auth.dto";
import { RoleEnum } from "src/entities/pengguna.entity";
import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
import { Roles } from "src/middlewares/roles.decorator";
import { RolesGuard } from "src/middlewares/roles.guard";
import {
DashboardDto,
GetDashboardDosbimQueryDto,
GetDashboardTimTesisReqQueryDto,
GetDashboardTimTesisRespDto,
JalurStatisticDto,
} from "./dashboard.dto";
import { DashboardService } from "./dashboard.service";
@ApiTags("Dashboard")
@ApiCookieAuth()
@ApiBearerAuth()
@Controller("dashboard")
@UseGuards(CustomAuthGuard, RolesGuard)
export class DashboardController {
constructor(private readonly dashboardService: DashboardService) {}
@ApiOkResponse({ type: [DashboardDto] })
@Roles(RoleEnum.S2_PEMBIMBING)
@Get("/dosbim")
async findByPenerimaId(
@Req() request: Request,
@Query() query: GetDashboardDosbimQueryDto,
): Promise<DashboardDto[]> {
return this.dashboardService.findByDosenId(
(request.user as AuthDto).id,
query.search,
);
}
@ApiOkResponse({ type: [JalurStatisticDto] })
@Roles(RoleEnum.S2_PEMBIMBING)
@Get("/dosbim/statistics")
async getStatisticsByJalurPilihan(@Req() request: Request) {
return this.dashboardService.getStatisticsByJalurPilihan(
(request.user as AuthDto).id,
);
}
@UseGuards(CustomAuthGuard, RolesGuard)
@Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN)
@ApiOkResponse({ type: GetDashboardTimTesisRespDto })
@Get("/tim-tesis")
async getDashboardTimTesis(
@Query() query: GetDashboardTimTesisReqQueryDto,
): Promise<GetDashboardTimTesisRespDto> {
return this.dashboardService.getDashboardTimTesis(query);
}
}
import {
ApiProperty,
ApiPropertyOptional,
OmitType,
PickType,
} from "@nestjs/swagger";
import { IsNumberString, IsOptional, IsString } from "class-validator";
import { BimbinganStatus } from "src/entities/bimbingan.entity";
import { Pengguna } from "src/entities/pengguna.entity";
import { Topik } from "src/entities/topik.entity";
import { JalurEnum } from "../entities/pendaftaranTesis.entity";
class PickedTopikDashboard extends PickType(Topik, ["id", "judul"] as const) {}
class PickedMhsDashboard extends PickType(Pengguna, [
"id",
"nama",
"nim",
"email",
] as const) {}
class NoEmailUserDashboard extends OmitType(PickedMhsDashboard, [
"email",
] as const) {}
export class DashboardDto {
@ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
id: string;
@ApiProperty({ enum: JalurEnum })
jalurPilihan: JalurEnum;
@ApiProperty({ enum: BimbinganStatus })
status: BimbinganStatus;
@ApiProperty()
topik: PickedTopikDashboard;
@ApiProperty()
mahasiswa: NoEmailUserDashboard;
}
export class JalurStatisticDto {
@ApiProperty({ enum: JalurEnum })
jalurPilihan: JalurEnum;
@ApiProperty()
count: number;
}
export class GetDashboardDosbimQueryDto {
@ApiPropertyOptional({})
@IsOptional()
search: string;
}
export class GetDashboardTimTesisReqQueryDto {
@IsOptional()
@IsNumberString()
@ApiPropertyOptional({ description: "default: 1" })
page?: number;
@IsOptional()
@IsNumberString()
@ApiPropertyOptional({ description: "default: no limit" })
limit?: number;
@IsOptional()
@IsString()
@ApiPropertyOptional()
search?: string;
}
export enum DashboardTimTesisStatusEnum {
PENGAJUAN_TOPIK = "PENGAJUAN_TOPIK",
SEMINAR_1 = "SEMINAR_1",
SEMINAR_2 = "SEMINAR_2",
SIDANG = "SIDANG",
}
class GetDashboardTimTesisDataDto {
@ApiProperty()
id_mahasiswa: string;
@ApiProperty()
nim_mahasiswa: string;
@ApiProperty()
nama_mahasiswa: string;
@ApiProperty({ isArray: true })
dosen_pembimbing: string[];
@ApiProperty({ isArray: true, enum: DashboardTimTesisStatusEnum })
status: DashboardTimTesisStatusEnum[];
}
export class GetDashboardTimTesisRespDto {
@ApiProperty()
maxPage: number;
@ApiProperty({ type: GetDashboardTimTesisDataDto })
data: GetDashboardTimTesisDataDto[];
}
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { BimbinganModule } from "src/bimbingan/bimbingan.module";
import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
import { PendaftaranSidsem } from "src/entities/pendaftaranSidsem";
import { PendaftaranTesis } from "../entities/pendaftaranTesis.entity";
import { Pengguna } from "../entities/pengguna.entity";
import { DashboardController } from "./dashboard.controller";
import { DashboardService } from "./dashboard.service";
@Module({
imports: [
TypeOrmModule.forFeature([
PendaftaranTesis,
Pengguna,
PendaftaranSidsem,
DosenBimbingan,
]),
BimbinganModule,
],
controllers: [DashboardController],
providers: [DashboardService],
})
export class DashboardModule {}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment