diff --git a/.env.example b/.env.example
index 60937fa941f657b5baedda90beed212a9592da3d..636ce5ecb1f6cbdeac73878e12263937afd06f5b 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/.gitlab/merge_request_templates/base.md b/.gitlab/merge_request_templates/base.md
new file mode 100644
index 0000000000000000000000000000000000000000..95e0d8dc200f0c4fd94720255c8d09e3d1607503
--- /dev/null
+++ b/.gitlab/merge_request_templates/base.md
@@ -0,0 +1,33 @@
+# 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 |
diff --git a/README.md b/README.md
index 00a13b112ad2bb1058f0581d956e7605f0b8bda0..63b887fa696e1111a6a8ff73dd9654ee2189b066 100644
--- a/README.md
+++ b/README.md
@@ -1,73 +1,154 @@
-<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.
diff --git a/package-lock.json b/package-lock.json
index ed3799a3fcf1ec45d7e2d40c205a4b94a1abd5c7..88b4e7cf5b71bf20a1847997a4f1f8261ae0640b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 1f1c5a9790adf7d2f639a8d849efafa899f05127..0d1f6272902f5c1add61fd2b56493b256439aba0 100644
--- a/package.json
+++ b/package.json
@@ -24,11 +24,21 @@
   },
   "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",
@@ -38,12 +48,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",
diff --git a/src/alokasi-topik/alokasi-topik.controller.ts b/src/alokasi-topik/alokasi-topik.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..069ae9fb2290d3831476d20cb7068beae81fbed4
--- /dev/null
+++ b/src/alokasi-topik/alokasi-topik.controller.ts
@@ -0,0 +1,174 @@
+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;
+  }
+}
diff --git a/src/alokasi-topik/alokasi-topik.dto.ts b/src/alokasi-topik/alokasi-topik.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5c41225c23ddf1fbefe996dfc7f961ad5ecebc01
--- /dev/null
+++ b/src/alokasi-topik/alokasi-topik.dto.ts
@@ -0,0 +1,87 @@
+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;
+}
diff --git a/src/alokasi-topik/alokasi-topik.module.ts b/src/alokasi-topik/alokasi-topik.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f7daaff6363cf0e2d4d1bcedeaea2c38023f6a67
--- /dev/null
+++ b/src/alokasi-topik/alokasi-topik.module.ts
@@ -0,0 +1,14 @@
+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 {}
diff --git a/src/alokasi-topik/alokasi-topik.service.ts b/src/alokasi-topik/alokasi-topik.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d52534adfd7f8155ba67dbf0ffe4684b689e9773
--- /dev/null
+++ b/src/alokasi-topik/alokasi-topik.service.ts
@@ -0,0 +1,145 @@
+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 });
+  }
+}
diff --git a/src/app.module.ts b/src/app.module.ts
index 171261220ecfc70b89c378e1e99dfedd349b039c..25728d3124a279326173b50c046e2cb22dc0ac8a 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -1,32 +1,33 @@
 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],
diff --git a/src/auth/auth.dto.ts b/src/auth/auth.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bd100449dd14ec85ce026bc5be22872248cef3c6
--- /dev/null
+++ b/src/auth/auth.dto.ts
@@ -0,0 +1,8 @@
+import { RoleEnum } from "src/entities/pengguna.entity";
+
+export class AuthDto {
+  id: string;
+  nama: string;
+  email: string;
+  roles: RoleEnum[];
+}
diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0691876b3e4130efa099cda32455ba2a4797e794
--- /dev/null
+++ b/src/auth/auth.module.ts
@@ -0,0 +1,10 @@
+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 {}
diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ca7049c75be0ca18491406211616531da0fd65a4
--- /dev/null
+++ b/src/auth/auth.service.ts
@@ -0,0 +1,28 @@
+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;
+  }
+}
diff --git a/src/bimbingan/bimbingan.controller.ts b/src/bimbingan/bimbingan.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4f03874b94e456fe8a890c47cc05661fb890835d
--- /dev/null
+++ b/src/bimbingan/bimbingan.controller.ts
@@ -0,0 +1,118 @@
+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,
+    );
+  }
+}
diff --git a/src/bimbingan/bimbingan.dto.ts b/src/bimbingan/bimbingan.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..649f9c95366f56738a281689e0520c876ec8cef7
--- /dev/null
+++ b/src/bimbingan/bimbingan.dto.ts
@@ -0,0 +1,108 @@
+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),
+) {}
diff --git a/src/bimbingan/bimbingan.module.ts b/src/bimbingan/bimbingan.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b77070fda6099172ac83112b4163af9a742f656d
--- /dev/null
+++ b/src/bimbingan/bimbingan.module.ts
@@ -0,0 +1,28 @@
+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 {}
diff --git a/src/bimbingan/bimbingan.service.ts b/src/bimbingan/bimbingan.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..825ce9109b5ac2c4be8f5e68aa0e600b2eda67e0
--- /dev/null
+++ b/src/bimbingan/bimbingan.service.ts
@@ -0,0 +1,254 @@
+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;
+    }
+  }
+}
diff --git a/src/dashboard/dashboard.controller.ts b/src/dashboard/dashboard.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8cec5c50418c02eac5f35279a28b7582647e149e
--- /dev/null
+++ b/src/dashboard/dashboard.controller.ts
@@ -0,0 +1,62 @@
+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);
+  }
+}
diff --git a/src/dashboard/dashboard.dto.ts b/src/dashboard/dashboard.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..adfb93e0e0a210e077c1704e260e90c164e15e7e
--- /dev/null
+++ b/src/dashboard/dashboard.dto.ts
@@ -0,0 +1,103 @@
+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[];
+}
diff --git a/src/dashboard/dashboard.module.ts b/src/dashboard/dashboard.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ad95d2fc648e9ebf0247fb7f326f682b9a1c8cf3
--- /dev/null
+++ b/src/dashboard/dashboard.module.ts
@@ -0,0 +1,24 @@
+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 {}
diff --git a/src/dashboard/dashboard.service.ts b/src/dashboard/dashboard.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..29e867e2227ed68513532f4fe109daf7c30fe550
--- /dev/null
+++ b/src/dashboard/dashboard.service.ts
@@ -0,0 +1,303 @@
+import { BadRequestException, Injectable } from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { BimbinganService } from "src/bimbingan/bimbingan.service";
+import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
+import {
+  PendaftaranSidsem,
+  SidsemStatus,
+  TipeSidsemEnum,
+} from "src/entities/pendaftaranSidsem";
+import { ArrayContains, Brackets, In, Like, Repository } from "typeorm";
+import {
+  PendaftaranTesis,
+  RegStatus,
+} from "../entities/pendaftaranTesis.entity";
+import { Pengguna, RoleEnum } from "../entities/pengguna.entity";
+import {
+  DashboardDto,
+  DashboardTimTesisStatusEnum,
+  GetDashboardTimTesisReqQueryDto,
+  GetDashboardTimTesisRespDto,
+  JalurStatisticDto,
+} from "./dashboard.dto";
+
+@Injectable()
+export class DashboardService {
+  constructor(
+    @InjectRepository(PendaftaranTesis)
+    private pendaftaranTesisRepository: Repository<PendaftaranTesis>,
+    @InjectRepository(Pengguna)
+    private penggunaRepository: Repository<Pengguna>,
+    @InjectRepository(DosenBimbingan)
+    private dosenBimbinganRepository: Repository<DosenBimbingan>,
+    @InjectRepository(PendaftaranSidsem)
+    private pendaftaranSidsemRepository: Repository<PendaftaranSidsem>,
+    private bimbinganService: BimbinganService,
+  ) {}
+
+  async findAll(): Promise<PendaftaranTesis[]> {
+    return this.pendaftaranTesisRepository.find({
+      relations: ["mahasiswa", "topik"],
+    });
+  }
+
+  async findByDosenId(
+    dosenId: string,
+    search?: string,
+  ): Promise<DashboardDto[]> {
+    let pendaftaranTesisQuery = this.pendaftaranTesisRepository
+      .createQueryBuilder("pendaftaranTesis")
+      .leftJoinAndSelect("pendaftaranTesis.mahasiswa", "mahasiswa")
+      .leftJoinAndSelect("pendaftaranTesis.topik", "topik")
+      .innerJoin(
+        "pendaftaranTesis.dosenBimbingan",
+        "dosenBimbingan",
+        "dosenBimbingan.idDosen = :dosenId",
+        {
+          dosenId,
+        },
+      )
+      .where("mahasiswa.aktif = true")
+      .andWhere("pendaftaranTesis.status = :status", {
+        status: RegStatus.APPROVED,
+      });
+
+    if (search) {
+      pendaftaranTesisQuery = pendaftaranTesisQuery.andWhere(
+        new Brackets((qb) => {
+          qb.where("mahasiswa.nama ILIKE :search", {
+            search: `%${search}%`,
+          }).orWhere("mahasiswa.nim ILIKE :search", { search: `%${search}%` });
+        }),
+      );
+    }
+    const pendaftaranTesis = await pendaftaranTesisQuery.getMany();
+
+    const statusMap = await Promise.all(
+      pendaftaranTesis.map(async (pendaftaran) => {
+        return await this.bimbinganService.getBimbinganStatus(pendaftaran);
+      }),
+    );
+
+    return pendaftaranTesis.map((pendaftaran, index) => {
+      return {
+        id: pendaftaran.id,
+        jalurPilihan: pendaftaran.jalurPilihan,
+        status: statusMap[index],
+        topik: {
+          id: pendaftaran.topik.id,
+          judul: pendaftaran.topik.judul,
+        },
+        mahasiswa: {
+          id: pendaftaran.mahasiswa.id,
+          nama: pendaftaran.mahasiswa.nama,
+          nim: pendaftaran.mahasiswa.nim,
+        },
+      };
+    });
+  }
+
+  async getStatisticsByJalurPilihan(
+    dosenId: string,
+  ): Promise<JalurStatisticDto[]> {
+    const dosen = await this.penggunaRepository.findOne({
+      where: { id: dosenId },
+    });
+    if (!dosen) {
+      throw new BadRequestException("Dosen tidak ditemukan");
+    }
+
+    const statistics = await this.pendaftaranTesisRepository
+      .createQueryBuilder("pendaftaranTesis")
+      .select("pendaftaranTesis.jalurPilihan", "jalurPilihan")
+      .addSelect("COUNT(*)", "count")
+      .leftJoin("pendaftaranTesis.mahasiswa", "mahasiswa")
+      .innerJoin(
+        "pendaftaranTesis.dosenBimbingan",
+        "dosenBimbingan",
+        "dosenBimbingan.idDosen = :dosenId",
+        {
+          dosenId,
+        },
+      )
+      .where("mahasiswa.aktif = true")
+      .andWhere("pendaftaranTesis.status = :status", {
+        status: RegStatus.APPROVED,
+      })
+      .groupBy("pendaftaranTesis.jalurPilihan")
+      .getRawMany();
+
+    return statistics as JalurStatisticDto[];
+  }
+
+  async getDashboardTimTesis(
+    query: GetDashboardTimTesisReqQueryDto,
+  ): Promise<GetDashboardTimTesisRespDto> {
+    const [foundMahasiswa, total] = await this.penggunaRepository.findAndCount({
+      select: {
+        id: true,
+        nama: true,
+        nim: true,
+      },
+      take: query.limit || undefined,
+      skip: (query.page - 1) * query.limit || undefined,
+      where: [
+        {
+          nim: Like(`%${query.search ?? ""}%`),
+          roles: ArrayContains([RoleEnum.S2_MAHASISWA]),
+          aktif: true,
+        },
+        {
+          nama: Like(`%${query.search ?? ""}%`),
+          roles: ArrayContains([RoleEnum.S2_MAHASISWA]),
+          aktif: true,
+        },
+      ],
+      order: {
+        nim: "ASC",
+      },
+    });
+
+    const dosbimQuery = this.dosenBimbinganRepository.find({
+      select: {
+        pendaftaran: {
+          id: true,
+          mahasiswaId: true,
+        },
+        dosen: {
+          id: true,
+          nama: true,
+        },
+      },
+      relations: {
+        pendaftaran: true,
+        dosen: true,
+      },
+      where: {
+        pendaftaran: {
+          mahasiswaId: In(foundMahasiswa.map(({ id }) => id)),
+        },
+      },
+    });
+
+    const topicAcceptedQuery = this.pendaftaranTesisRepository.find({
+      select: {
+        id: true,
+        mahasiswaId: true,
+      },
+      where: {
+        status: RegStatus.APPROVED,
+        mahasiswaId: In(foundMahasiswa.map(({ id }) => id)),
+      },
+    });
+
+    const mhsSemProAcceptedQuery = this.pendaftaranSidsemRepository.find({
+      select: {
+        pendaftaranTesis: {
+          mahasiswaId: true,
+        },
+      },
+      relations: {
+        pendaftaranTesis: true,
+      },
+      where: {
+        tipe: TipeSidsemEnum.SEMINAR_1,
+        status: SidsemStatus.APPROVED,
+        pendaftaranTesis: {
+          mahasiswaId: In(foundMahasiswa.map(({ id }) => id)),
+        },
+      },
+    });
+
+    const mhsSemTesAcceptedQuery = this.pendaftaranSidsemRepository.find({
+      select: {
+        pendaftaranTesis: {
+          mahasiswaId: true,
+        },
+      },
+      relations: {
+        pendaftaranTesis: true,
+      },
+      where: {
+        tipe: TipeSidsemEnum.SEMINAR_2,
+        status: SidsemStatus.APPROVED,
+        pendaftaranTesis: {
+          mahasiswaId: In(foundMahasiswa.map(({ id }) => id)),
+        },
+      },
+    });
+
+    const mhsSidangAcceptedQuery = this.pendaftaranSidsemRepository.find({
+      select: {
+        pendaftaranTesis: {
+          mahasiswaId: true,
+        },
+      },
+      relations: {
+        pendaftaranTesis: true,
+      },
+      where: {
+        tipe: TipeSidsemEnum.SIDANG,
+        status: SidsemStatus.APPROVED,
+        pendaftaranTesis: {
+          mahasiswaId: In(foundMahasiswa.map(({ id }) => id)),
+        },
+      },
+    });
+
+    const [
+      foundDosbim,
+      topicAccepted,
+      mhsSemProAccepted,
+      mhsSemTesAccepted,
+      mhsSidangAccepted,
+    ] = await Promise.all([
+      dosbimQuery,
+      topicAcceptedQuery,
+      mhsSemProAcceptedQuery,
+      mhsSemTesAcceptedQuery,
+      mhsSidangAcceptedQuery,
+    ]);
+
+    const mhsStatusMap: Record<string, DashboardTimTesisStatusEnum[]> = {};
+    const dosbimMap: Record<string, string[]> = {};
+
+    foundMahasiswa.forEach(({ id }) => {
+      mhsStatusMap[id] = [];
+      dosbimMap[id] = [];
+    });
+
+    foundDosbim.forEach(({ pendaftaran: { mahasiswaId }, dosen: { nama } }) => {
+      dosbimMap[mahasiswaId].push(nama);
+    });
+
+    topicAccepted.forEach(({ mahasiswaId }) => {
+      mhsStatusMap[mahasiswaId].push(
+        DashboardTimTesisStatusEnum.PENGAJUAN_TOPIK,
+      );
+    });
+
+    mhsSemProAccepted.forEach(({ pendaftaranTesis: { mahasiswaId } }) => {
+      mhsStatusMap[mahasiswaId].push(DashboardTimTesisStatusEnum.SEMINAR_1);
+    });
+
+    mhsSemTesAccepted.forEach(({ pendaftaranTesis: { mahasiswaId } }) => {
+      mhsStatusMap[mahasiswaId].push(DashboardTimTesisStatusEnum.SEMINAR_2);
+    });
+
+    mhsSidangAccepted.forEach(({ pendaftaranTesis: { mahasiswaId } }) => {
+      mhsStatusMap[mahasiswaId].push(DashboardTimTesisStatusEnum.SIDANG);
+    });
+
+    return {
+      maxPage: !!query.limit ? Math.ceil(total / query.limit) : 1,
+      data: foundMahasiswa.map(({ nim, id, nama }) => ({
+        id_mahasiswa: id,
+        nim_mahasiswa: nim,
+        nama_mahasiswa: nama,
+        status: mhsStatusMap[id] ?? [],
+        dosen_pembimbing: dosbimMap[id] ?? [],
+      })),
+    };
+  }
+}
diff --git a/src/dosen-bimbingan/dosen-bimbingan.controller.ts b/src/dosen-bimbingan/dosen-bimbingan.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..81a31679c5443f03c6469b478be79566aa2e80b0
--- /dev/null
+++ b/src/dosen-bimbingan/dosen-bimbingan.controller.ts
@@ -0,0 +1,34 @@
+import { Controller, Get, UseGuards } from "@nestjs/common";
+import {
+  ApiBearerAuth,
+  ApiCookieAuth,
+  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 { GetDosbimResDto } from "./dosen-bimbingan.dto";
+import { DosenBimbinganService } from "./dosen-bimbingan.service";
+
+@ApiTags("Dosen Bimbingan")
+@ApiCookieAuth()
+@ApiBearerAuth()
+@Controller("dosen-bimbingan")
+@UseGuards(CustomAuthGuard, RolesGuard)
+@Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_MAHASISWA)
+export class DosenBimbinganController {
+  constructor(private readonly dosbimService: DosenBimbinganService) {}
+
+  @ApiOkResponse({ type: [GetDosbimResDto] })
+  @ApiOperation({
+    summary:
+      "Get all available dosen bimbingan. Roles: ADMIN, S2_TIM_TESIS, S2_MAHASISWA",
+  })
+  @Get()
+  async get() {
+    return await this.dosbimService.getAll();
+  }
+}
diff --git a/src/dosen-bimbingan/dosen-bimbingan.dto.ts b/src/dosen-bimbingan/dosen-bimbingan.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..47a532f830f4d2f30afeff37c5e57b5a6a3e4b82
--- /dev/null
+++ b/src/dosen-bimbingan/dosen-bimbingan.dto.ts
@@ -0,0 +1,9 @@
+import { PickType } from "@nestjs/swagger";
+import { Pengguna } from "src/entities/pengguna.entity";
+
+export class GetDosbimResDto extends PickType(Pengguna, [
+  "id",
+  "email",
+  "nama",
+  "keahlian",
+] as const) {}
diff --git a/src/dosen-bimbingan/dosen-bimbingan.module.ts b/src/dosen-bimbingan/dosen-bimbingan.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1721d9d65412532695c2c187b62f6aec9e248070
--- /dev/null
+++ b/src/dosen-bimbingan/dosen-bimbingan.module.ts
@@ -0,0 +1,14 @@
+import { Module } from "@nestjs/common";
+import { DosenBimbinganController } from "./dosen-bimbingan.controller";
+import { DosenBimbinganService } from "./dosen-bimbingan.service";
+import { AuthModule } from "src/auth/auth.module";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { CustomStrategy } from "src/middlewares/custom.strategy";
+
+@Module({
+  imports: [TypeOrmModule.forFeature([Pengguna]), AuthModule],
+  controllers: [DosenBimbinganController],
+  providers: [DosenBimbinganService, CustomStrategy],
+})
+export class DosenBimbinganModule {}
diff --git a/src/dosen-bimbingan/dosen-bimbingan.service.ts b/src/dosen-bimbingan/dosen-bimbingan.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9d9039a1dbc62780d044cb2fac6f09239e2ebc07
--- /dev/null
+++ b/src/dosen-bimbingan/dosen-bimbingan.service.ts
@@ -0,0 +1,26 @@
+import { Injectable } from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
+import { ArrayContains, Repository } from "typeorm";
+
+@Injectable()
+export class DosenBimbinganService {
+  constructor(
+    @InjectRepository(Pengguna)
+    private penggunaRepo: Repository<Pengguna>,
+  ) {}
+
+  async getAll() {
+    return await this.penggunaRepo.find({
+      select: {
+        id: true,
+        nama: true,
+        email: true,
+        keahlian: true,
+      },
+      where: {
+        roles: ArrayContains([RoleEnum.S2_PEMBIMBING]),
+      },
+    });
+  }
+}
diff --git a/src/dosen-penguji/dosen-penguji.controller.ts b/src/dosen-penguji/dosen-penguji.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d50cb97c330b731985268617b2e50fe6087c7066
--- /dev/null
+++ b/src/dosen-penguji/dosen-penguji.controller.ts
@@ -0,0 +1,33 @@
+import { Controller, Get, UseGuards } from "@nestjs/common";
+import {
+  ApiBearerAuth,
+  ApiCookieAuth,
+  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 { GetDosujiResDto } from "./dosen-penguji.dto";
+import { DosenPengujiService } from "./dosen-penguji.service";
+
+@ApiTags("Dosen Penguji")
+@ApiCookieAuth()
+@ApiBearerAuth()
+@Controller("dosen-penguji")
+@UseGuards(CustomAuthGuard, RolesGuard)
+@Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+export class DosenPengujiController {
+  constructor(private readonly dosujiService: DosenPengujiService) {}
+
+  @ApiOkResponse({ type: [GetDosujiResDto] })
+  @ApiOperation({
+    summary: "Get all available dosen penguji. Roles: ADMIN, S2_TIM_TESIS",
+  })
+  @Get()
+  async get() {
+    return await this.dosujiService.getAll();
+  }
+}
diff --git a/src/dosen-penguji/dosen-penguji.dto.ts b/src/dosen-penguji/dosen-penguji.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..99edd73b7e11ddf929d78756346cdf4efd0913fb
--- /dev/null
+++ b/src/dosen-penguji/dosen-penguji.dto.ts
@@ -0,0 +1,9 @@
+import { PickType } from "@nestjs/swagger";
+import { Pengguna } from "src/entities/pengguna.entity";
+
+export class GetDosujiResDto extends PickType(Pengguna, [
+  "id",
+  "email",
+  "nama",
+  "keahlian",
+] as const) {}
diff --git a/src/dosen-penguji/dosen-penguji.module.ts b/src/dosen-penguji/dosen-penguji.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1c64d3a567c20ecd9f1e949e6f99d7e998db7d13
--- /dev/null
+++ b/src/dosen-penguji/dosen-penguji.module.ts
@@ -0,0 +1,14 @@
+import { Module } from "@nestjs/common";
+import { AuthModule } from "src/auth/auth.module";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { CustomStrategy } from "src/middlewares/custom.strategy";
+import { DosenPengujiController } from "./dosen-penguji.controller";
+import { DosenPengujiService } from "./dosen-penguji.service";
+
+@Module({
+  imports: [TypeOrmModule.forFeature([Pengguna]), AuthModule],
+  controllers: [DosenPengujiController],
+  providers: [DosenPengujiService, CustomStrategy],
+})
+export class DosenPengujiModule {}
diff --git a/src/dosen-penguji/dosen-penguji.service.ts b/src/dosen-penguji/dosen-penguji.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..712b5ced205297b509b4d23820557969a7f51888
--- /dev/null
+++ b/src/dosen-penguji/dosen-penguji.service.ts
@@ -0,0 +1,26 @@
+import { Injectable } from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
+import { ArrayContains, Repository } from "typeorm";
+
+@Injectable()
+export class DosenPengujiService {
+  constructor(
+    @InjectRepository(Pengguna)
+    private penggunaRepo: Repository<Pengguna>,
+  ) {}
+
+  async getAll() {
+    return await this.penggunaRepo.find({
+      select: {
+        id: true,
+        nama: true,
+        email: true,
+        keahlian: true,
+      },
+      where: {
+        roles: ArrayContains([RoleEnum.S2_PENGUJI]),
+      },
+    });
+  }
+}
diff --git a/src/dto/registrasi-topik.ts b/src/dto/registrasi-topik.ts
deleted file mode 100644
index 3d0021750739795336deb7d08c75ee7d14f36141..0000000000000000000000000000000000000000
--- a/src/dto/registrasi-topik.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export class RegistrasiTopikDto {
-  idMahasiswa: string;
-  idPembimbing: string;
-  judulTopik: string;
-  deskripsi: string;
-  jalurPilihan: string;
-}
diff --git a/src/entities/auditLog.entity.ts b/src/entities/auditLog.entity.ts
deleted file mode 100644
index c95d05a03fb11fb13d835bc1e6c64b22e107bff6..0000000000000000000000000000000000000000
--- a/src/entities/auditLog.entity.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class AuditLog {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  pengguna: Pengguna;
-}
diff --git a/src/entities/berkasBimbingan.entity.ts b/src/entities/berkasBimbingan.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..35b44ace3f7a70db3a1bcbd210c3a95be701c99a
--- /dev/null
+++ b/src/entities/berkasBimbingan.entity.ts
@@ -0,0 +1,26 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import { Bimbingan } from "./bimbingan.entity";
+import { IsString, IsUrl } from "@nestjs/class-validator";
+import { ApiProperty } from "@nestjs/swagger";
+
+@Entity()
+export class BerkasBimbingan {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ManyToOne(() => Bimbingan, (bimbingan) => bimbingan.id, {
+    orphanedRowAction: "delete",
+  })
+  bimbingan: Bimbingan;
+
+  @Column({ type: "text" })
+  @IsString()
+  @ApiProperty()
+  nama: string;
+
+  @Column({ type: "text" })
+  @IsUrl()
+  @ApiProperty({ example: "https://example.com/berkas.pdf" })
+  url: string;
+}
diff --git a/src/entities/berkasSidsem.entity.ts b/src/entities/berkasSidsem.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7105028e2c852feb7127e4d7837236f2752ec58d
--- /dev/null
+++ b/src/entities/berkasSidsem.entity.ts
@@ -0,0 +1,30 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import { IsString, IsUrl } from "@nestjs/class-validator";
+import { ApiProperty } from "@nestjs/swagger";
+import { PendaftaranSidsem } from "./pendaftaranSidsem";
+
+@Entity()
+export class BerkasSidsem {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ManyToOne(
+    () => PendaftaranSidsem,
+    (pendaftaranSidsem) => pendaftaranSidsem.id,
+    {
+      orphanedRowAction: "delete",
+    },
+  )
+  pendaftaranSidsem: PendaftaranSidsem;
+
+  @Column({ type: "text" })
+  @IsString()
+  @ApiProperty()
+  nama: string;
+
+  @Column({ type: "text" })
+  @IsUrl()
+  @ApiProperty({ example: "https://example.com/berkas.pdf" })
+  url: string;
+}
diff --git a/src/entities/bimbingan.entity.ts b/src/entities/bimbingan.entity.ts
index 997e08193353a428c388ee21ee96a9c4ca146020..a846e824d3d98389e6a2402efad351f49188f9fb 100644
--- a/src/entities/bimbingan.entity.ts
+++ b/src/entities/bimbingan.entity.ts
@@ -1,15 +1,56 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Topik } from "./topik.entity";
-import { Pengguna } from "./pengguna.entity";
+import {
+  Column,
+  Entity,
+  ManyToOne,
+  OneToMany,
+  PrimaryGeneratedColumn,
+} from "typeorm";
+import { PendaftaranTesis } from "./pendaftaranTesis.entity";
+import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import { BerkasBimbingan } from "./berkasBimbingan.entity";
+
+export enum BimbinganStatus {
+  LANCAR = "LANCAR",
+  BUTUH_BIMBINGAN = "BUTUH_BIMBINGAN",
+  TERKENDALA = "TERKENDALA",
+}
 
 @Entity()
 export class Bimbingan {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
-  @ManyToOne(() => Topik, (topik) => topik.id)
-  topik: Topik;
+  @ApiProperty({ type: Date })
+  @Column({ type: "date" })
+  waktuBimbingan: string;
+
+  @ApiProperty()
+  @Column({ type: "text" })
+  laporanKemajuan: string;
+
+  @ApiProperty()
+  @Column({ type: "text" })
+  todo: string;
+
+  @ApiPropertyOptional({ type: Date })
+  @Column({ type: "date", nullable: true })
+  bimbinganBerikutnya: string;
+
+  @ApiProperty()
+  @Column({ type: "boolean", default: true })
+  disahkan: boolean;
+
+  @ManyToOne(() => PendaftaranTesis, (pendaftaranTesis) => pendaftaranTesis.id)
+  pendaftaran: PendaftaranTesis;
 
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
+  @ApiProperty({ type: [BerkasBimbingan] })
+  @OneToMany(
+    () => BerkasBimbingan,
+    (berkasBimbingan) => berkasBimbingan.bimbingan,
+    {
+      cascade: true,
+    },
+  )
+  berkas: BerkasBimbingan[];
 }
diff --git a/src/entities/dosenBimbingan.entity.ts b/src/entities/dosenBimbingan.entity.ts
index 50450bf3ad46f912b6b387944240e0d15a93c318..a43bc7bd6b7dd7e5d69b7bf9f98803707c66dd34 100644
--- a/src/entities/dosenBimbingan.entity.ts
+++ b/src/entities/dosenBimbingan.entity.ts
@@ -1,15 +1,29 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Bimbingan } from "./bimbingan.entity";
+import {
+  Column,
+  Entity,
+  JoinColumn,
+  ManyToOne,
+  PrimaryGeneratedColumn,
+} from "typeorm";
 import { Pengguna } from "./pengguna.entity";
+import { PendaftaranTesis } from "./pendaftaranTesis.entity";
 
 @Entity()
 export class DosenBimbingan {
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
-  @ManyToOne(() => Bimbingan, (bimbingan) => bimbingan.id)
-  bimbingan: Bimbingan;
+  @ManyToOne(() => PendaftaranTesis, (pendaftaranTesis) => pendaftaranTesis.id)
+  @JoinColumn({ name: "idPendaftaran" })
+  pendaftaran: PendaftaranTesis;
+
+  @Column({ nullable: true })
+  idPendaftaran: string;
 
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "idDosen" })
   dosen: Pengguna;
+
+  @Column({ nullable: true })
+  idDosen: string;
 }
diff --git a/src/entities/kelas.entity.ts b/src/entities/kelas.entity.ts
deleted file mode 100644
index 00fcfc4d8fa30479dd969a1bfeb629b6bb72bc78..0000000000000000000000000000000000000000
--- a/src/entities/kelas.entity.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Entity, PrimaryGeneratedColumn } from "typeorm";
-
-@Entity()
-export class Kelas {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-}
diff --git a/src/entities/konfigurasi.entity.ts b/src/entities/konfigurasi.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..26ee3cee556d63ef6ae248c1f957afe0c56254f8
--- /dev/null
+++ b/src/entities/konfigurasi.entity.ts
@@ -0,0 +1,27 @@
+import { ApiProperty } from "@nestjs/swagger";
+import { IsEnum, IsString } from "class-validator";
+import { Column, Entity, PrimaryColumn } from "typeorm";
+
+export enum KonfigurasiKeyEnum {
+  AWAL_PENDAFTARAN = "AWAL_PENDAFTARAN",
+  AKHIR_PENDAFTARAN = "AKHIR_PENDAFTARAN",
+  AWAL_SEMPRO = "AWAL_SEMPRO",
+  AKHIR_SEMPRO = "AKHIR_SEMPRO",
+  AWAL_SEM_TESIS = "AWAL_SEM_TESIS",
+  AKHIR_SEM_TESIS = "AKHIR_SEM_TESIS",
+  AWAL_SIDANG = "AWAL_SIDANG",
+  AKHIR_SIDANG = "AKHIR_SIDANG",
+}
+
+@Entity()
+export class Konfigurasi {
+  @ApiProperty({ enum: KonfigurasiKeyEnum })
+  @IsEnum(KonfigurasiKeyEnum)
+  @PrimaryColumn({ type: "enum", enum: KonfigurasiKeyEnum })
+  key: KonfigurasiKeyEnum;
+
+  @ApiProperty()
+  @IsString()
+  @Column({ type: "text" })
+  value: string;
+}
diff --git a/src/entities/mahasiswaKelas.ts b/src/entities/mahasiswaKelas.ts
deleted file mode 100644
index 2c8422498639304cb86e2cb7f7eefacc3614b38d..0000000000000000000000000000000000000000
--- a/src/entities/mahasiswaKelas.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Kelas } from "./kelas.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class MahasiswaKelas {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Kelas, (kelas) => kelas.id)
-  kelas: Kelas;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
-}
diff --git a/src/entities/pembimbingSeminar.entity.ts b/src/entities/pembimbingSeminar.entity.ts
deleted file mode 100644
index 024ebe5b57e6835441faa33d687137fb82565cbb..0000000000000000000000000000000000000000
--- a/src/entities/pembimbingSeminar.entity.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Seminar } from "./seminar.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class PembimbingSeminar {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Seminar, (seminar) => seminar.id)
-  seminar: Seminar;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  dosen: Pengguna;
-}
diff --git a/src/entities/pembimbingSidang.entity.ts b/src/entities/pembimbingSidang.entity.ts
deleted file mode 100644
index a3d9dbec55b4d3e94d7a1e6d8b14262b10752e3d..0000000000000000000000000000000000000000
--- a/src/entities/pembimbingSidang.entity.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Sidang } from "./sidang.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class PembimbingSidang {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Sidang, (sidang) => sidang.id)
-  sidang: Sidang;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  dosen: Pengguna;
-}
diff --git a/src/entities/pendaftaranSidsem.ts b/src/entities/pendaftaranSidsem.ts
new file mode 100644
index 0000000000000000000000000000000000000000..901bcd467d91ca787f2cbb58f0cb70f7ad19291b
--- /dev/null
+++ b/src/entities/pendaftaranSidsem.ts
@@ -0,0 +1,101 @@
+import {
+  Column,
+  Entity,
+  ManyToOne,
+  OneToMany,
+  PrimaryGeneratedColumn,
+} from "typeorm";
+import { PendaftaranTesis } from "./pendaftaranTesis.entity";
+// import { Ruangan } from "./ruangan.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { PengujiSidsem } from "./pengujiSidsem.entity";
+import { BerkasSidsem } from "./berkasSidsem.entity";
+import { IsEnum, IsUUID } from "@nestjs/class-validator";
+import { IsDateString, IsNotEmpty, IsString } from "class-validator";
+
+export enum TipeSidsemEnum {
+  SEMINAR_1 = "SEMINAR_1",
+  SEMINAR_2 = "SEMINAR_2",
+  SIDANG = "SIDANG",
+}
+
+export enum SidsemStatus {
+  NOT_ASSIGNED = "NOT_ASSIGNED",
+  REJECTED = "REJECTED",
+  APPROVED = "APPROVED",
+}
+
+@Entity()
+export class PendaftaranSidsem {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ApiProperty({ enum: TipeSidsemEnum })
+  @IsEnum(TipeSidsemEnum)
+  @Column({ type: "enum", enum: TipeSidsemEnum })
+  tipe: TipeSidsemEnum;
+
+  @ApiProperty()
+  @IsEnum(SidsemStatus)
+  @Column({
+    type: "enum",
+    enum: SidsemStatus,
+    default: SidsemStatus.NOT_ASSIGNED,
+  })
+  status: SidsemStatus;
+
+  @ApiProperty()
+  @IsDateString()
+  @Column({ type: "timestamptz", nullable: true })
+  jadwal: Date;
+
+  @ApiProperty()
+  @IsString()
+  @IsNotEmpty()
+  @Column({ type: "text" })
+  judulSidsem: string;
+
+  @ApiProperty()
+  @IsString()
+  @IsNotEmpty()
+  @Column({ type: "text" })
+  deskripsiSidsem: string;
+
+  @ManyToOne(() => PendaftaranTesis, (pendaftaranTesis) => pendaftaranTesis.id)
+  pendaftaranTesis: PendaftaranTesis;
+
+  @ApiProperty()
+  @IsString()
+  @IsNotEmpty()
+  @Column({ type: "text", nullable: true })
+  ruangan: string;
+
+  @OneToMany(() => PengujiSidsem, (pengujiSidsem) => pengujiSidsem.sidsem)
+  penguji: PengujiSidsem[];
+
+  @ApiProperty({ type: [BerkasSidsem] })
+  @OneToMany(
+    () => BerkasSidsem,
+    (berkasSidsem) => berkasSidsem.pendaftaranSidsem,
+    {
+      cascade: true,
+    },
+  )
+  berkasSidsem: BerkasSidsem[];
+
+  @ApiProperty()
+  @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
+  waktuPengiriman: Date;
+}
+
+export function cmpTipeSidsem(a: TipeSidsemEnum, b: TipeSidsemEnum): number {
+  const tipeSidsemOrder = {
+    [TipeSidsemEnum.SEMINAR_1]: 1,
+    [TipeSidsemEnum.SEMINAR_2]: 2,
+    [TipeSidsemEnum.SIDANG]: 3,
+  };
+
+  return tipeSidsemOrder[a] - tipeSidsemOrder[b];
+}
diff --git a/src/entities/pendaftaranTesis.entity.ts b/src/entities/pendaftaranTesis.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e9a8fa31c2f525bcc45c5d87cf250d018901ceda
--- /dev/null
+++ b/src/entities/pendaftaranTesis.entity.ts
@@ -0,0 +1,80 @@
+import {
+  Column,
+  Entity,
+  JoinColumn,
+  ManyToOne,
+  OneToMany,
+  PrimaryGeneratedColumn,
+} from "typeorm";
+import { Pengguna } from "./pengguna.entity";
+import { Topik } from "./topik.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { DosenBimbingan } from "./dosenBimbingan.entity";
+
+export enum RegStatus {
+  NOT_ASSIGNED = "NOT_ASSIGNED",
+  INTERVIEW = "INTERVIEW",
+  APPROVED = "APPROVED",
+  REJECTED = "REJECTED",
+}
+
+export enum JalurEnum {
+  CS = "CS",
+  SEI = "SEI",
+  IS = "IS",
+  IT = "IT",
+  INTS = "INTS",
+  MMT = "MMT",
+  CC = "CC",
+  DSAI = "DSAI",
+  CSEC = "CSEC",
+}
+
+@Entity()
+export class PendaftaranTesis {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ApiProperty({ enum: JalurEnum })
+  @Column({ type: "enum", enum: JalurEnum })
+  jalurPilihan: JalurEnum;
+
+  @ApiProperty()
+  @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
+  waktuPengiriman: Date;
+
+  @ApiProperty()
+  @Column({ type: "timestamptz", nullable: true })
+  jadwalInterview: Date;
+
+  @ApiProperty()
+  @Column({ type: "timestamptz", nullable: true })
+  waktuKeputusan: Date;
+
+  @ApiProperty({ enum: RegStatus })
+  @Column({ type: "enum", enum: RegStatus, default: RegStatus.NOT_ASSIGNED })
+  status: RegStatus;
+
+  @ApiProperty({ type: Topik })
+  @ManyToOne(() => Topik, (topik) => topik.id, { cascade: true })
+  topik: Topik;
+
+  @ApiProperty()
+  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "mahasiswaId" })
+  mahasiswa: Pengguna;
+
+  @Column()
+  mahasiswaId: string;
+
+  @ApiProperty()
+  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  penerima: Pengguna;
+
+  @OneToMany(
+    () => DosenBimbingan,
+    (dosenBimbingan) => dosenBimbingan.pendaftaran,
+  )
+  dosenBimbingan: DosenBimbingan[];
+}
diff --git a/src/entities/pengajarKelas.entity.ts b/src/entities/pengajarKelas.entity.ts
deleted file mode 100644
index ad5b1351996cf9c2dba15bcd5aff65d270fa1e74..0000000000000000000000000000000000000000
--- a/src/entities/pengajarKelas.entity.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Kelas } from "./kelas.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class PengajarKelas {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Kelas, (kelas) => kelas.id)
-  kelas: Kelas;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  dosen: Pengguna;
-}
diff --git a/src/entities/pengajuanPengambilanTopik.entity.ts b/src/entities/pengajuanPengambilanTopik.entity.ts
deleted file mode 100644
index 01a193286188f877065398c2749e17e4937ee797..0000000000000000000000000000000000000000
--- a/src/entities/pengajuanPengambilanTopik.entity.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Topik } from "./topik.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class PengajuanPengambilanTopik {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @Column({ nullable: true })
-  disahkan: boolean;
-
-  @Column({ nullable: true })
-  deskripsi: string;
-
-  @Column()
-  jalurPilihan: string;
-
-  @Column({ type: "timestamptz" })
-  waktuPengiriman: Date;
-
-  @Column({ type: "timestamptz", nullable: true })
-  waktuPersetujuan: Date;
-
-  @Column({ type: "timestamptz", nullable: true })
-  jadwalInterview: Date;
-
-  @Column({ type: "timestamptz", nullable: true })
-  waktuPengesahan: Date;
-
-  @Column({ type: "timestamptz", nullable: true })
-  waktuPenolakan: Date;
-
-  @ManyToOne(() => Topik, (topik) => topik.id)
-  topik: Topik;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  pembimbing: Pengguna;
-}
diff --git a/src/entities/pengambilanTopik.entity.ts b/src/entities/pengambilanTopik.entity.ts
deleted file mode 100644
index 6e933ed1214f0e1312076963fd7270719c5acb9c..0000000000000000000000000000000000000000
--- a/src/entities/pengambilanTopik.entity.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Topik } from "./topik.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class PengambilanTopik {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @Column()
-  deskripsi: string;
-
-  @Column()
-  jalurPilihan: string;
-
-  @ManyToOne(() => Topik, (topik) => topik.id)
-  topik: Topik;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  pembimbing: Pengguna;
-}
diff --git a/src/entities/pengguna.entity.ts b/src/entities/pengguna.entity.ts
index 4ff51c2dcc29e2e6e0a30cf1aafd8e76303f910c..4018370c6e6f5550a3d8126b08ad179c9a78f0c4 100644
--- a/src/entities/pengguna.entity.ts
+++ b/src/entities/pengguna.entity.ts
@@ -1,19 +1,86 @@
-import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
+import {
+  ApiHideProperty,
+  ApiProperty,
+  ApiPropertyOptional,
+} from "@nestjs/swagger";
+import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
+import { PendaftaranTesis } from "./pendaftaranTesis.entity";
+import { DosenBimbingan } from "./dosenBimbingan.entity";
+
+export enum RoleEnum {
+  ADMIN = "ADMIN",
+  S2_MAHASISWA = "S2_MAHASISWA",
+  S2_PEMBIMBING = "S2_PEMBIMBING",
+  S2_PENGUJI = "S2_PENGUJI",
+  S2_TIM_TESIS = "S2_TIM_TESIS",
+  S1_MAHASISWA = "S1_MAHASISWA",
+  S1_PEMBIMBING = "S1_PEMBIMBING",
+  S1_PENGUJI = "S1_PENGUJI",
+  S1_TIM_TA = "S1_TIM_TA",
+}
 
 @Entity()
 export class Pengguna {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
-  @Column()
+  @ApiProperty()
+  @Column({ type: "text", nullable: true })
   nama: string;
 
-  @Column()
+  @ApiProperty({ example: "13521999@mahasiswa.itb.ac.id" })
+  @Column({ type: "text", unique: true })
   email: string;
 
-  @Column()
-  status: string;
+  @ApiHideProperty()
+  @Column({ type: "text", nullable: true })
+  password: string;
+
+  @ApiPropertyOptional({ example: "13521999" })
+  @Column({ type: "varchar", length: 8, nullable: true })
+  nim: string;
+
+  @ApiProperty({ enum: RoleEnum, isArray: true })
+  @Column({
+    type: "enum",
+    enum: RoleEnum,
+    array: true,
+    default: [],
+  })
+  roles: RoleEnum[];
+
+  @ApiPropertyOptional()
+  @Column({ type: "text", nullable: true })
+  kontakWhatsApp: string;
+
+  @ApiPropertyOptional()
+  @Column({ type: "text", nullable: true })
+  kontakMsTeams: string;
+
+  @ApiPropertyOptional()
+  @Column({ type: "text", nullable: true })
+  kontakEmail: string;
+
+  @ApiPropertyOptional()
+  @Column({ type: "text", nullable: true })
+  kontakTelp: string;
+
+  @ApiPropertyOptional()
+  @Column({ type: "text", nullable: true })
+  kontakLainnya: string;
+
+  @ApiPropertyOptional()
+  @Column({ type: "text", nullable: true })
+  keahlian: string;
+
+  @ApiHideProperty()
+  @Column({ type: "boolean", default: true })
+  aktif: boolean;
+
+  @OneToMany(() => PendaftaranTesis, (pendaftaran) => pendaftaran.mahasiswa)
+  pendaftaranTesis: PendaftaranTesis[];
 
-  @Column("simple-array")
-  roles: string[];
+  @OneToMany(() => DosenBimbingan, (dosen) => dosen.dosen)
+  dosenBimbingan: DosenBimbingan[];
 }
diff --git a/src/entities/pengujiSidang.entity.ts b/src/entities/pengujiSidang.entity.ts
deleted file mode 100644
index dfcd36b5c2f5e06bebad302b3488aedff86338c7..0000000000000000000000000000000000000000
--- a/src/entities/pengujiSidang.entity.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Sidang } from "./sidang.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class PengujiSidang {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Sidang, (sidang) => sidang.id)
-  sidang: Sidang;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  dosen: Pengguna;
-}
diff --git a/src/entities/pengujiSidsem.entity.ts b/src/entities/pengujiSidsem.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..934f0da7dbfc9520ee87e55c12492db4575a203b
--- /dev/null
+++ b/src/entities/pengujiSidsem.entity.ts
@@ -0,0 +1,32 @@
+import {
+  Column,
+  Entity,
+  JoinColumn,
+  ManyToOne,
+  PrimaryGeneratedColumn,
+} from "typeorm";
+import { Pengguna } from "./pengguna.entity";
+import { PendaftaranSidsem } from "./pendaftaranSidsem";
+
+@Entity()
+export class PengujiSidsem {
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ManyToOne(
+    () => PendaftaranSidsem,
+    (pendaftaranSidsem) => pendaftaranSidsem.id,
+  )
+  @JoinColumn({ name: "idSidsem" })
+  sidsem: PendaftaranSidsem;
+
+  @Column()
+  idSidsem: string;
+
+  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "idDosen" })
+  dosen: Pengguna;
+
+  @Column()
+  idDosen: string;
+}
diff --git a/src/entities/rangeJadwalSeminar.entity.ts b/src/entities/rangeJadwalSeminar.entity.ts
deleted file mode 100644
index fa16bf749d40cdfeff7253f61bc098063a7348a3..0000000000000000000000000000000000000000
--- a/src/entities/rangeJadwalSeminar.entity.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Entity, PrimaryGeneratedColumn } from "typeorm";
-
-@Entity()
-export class RangeJadwalSeminar {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-}
diff --git a/src/entities/rangeJadwalSidang.entity.ts b/src/entities/rangeJadwalSidang.entity.ts
deleted file mode 100644
index 6479aef875d9d7b23cd9db1c00317af5e60756cd..0000000000000000000000000000000000000000
--- a/src/entities/rangeJadwalSidang.entity.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Entity, PrimaryGeneratedColumn } from "typeorm";
-
-@Entity()
-export class RangeJadwalSidang {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-}
diff --git a/src/entities/ruangan.entity.ts b/src/entities/ruangan.entity.ts
deleted file mode 100644
index 97df4c092f30bd329d8c3ece86d385bd0e6300c2..0000000000000000000000000000000000000000
--- a/src/entities/ruangan.entity.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Entity, PrimaryGeneratedColumn } from "typeorm";
-
-@Entity()
-export class Ruangan {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-}
diff --git a/src/entities/seminar.entity.ts b/src/entities/seminar.entity.ts
deleted file mode 100644
index 7b0e30b783be1e1401448e73f5fd078f7d4cdd0c..0000000000000000000000000000000000000000
--- a/src/entities/seminar.entity.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Pengguna } from "./pengguna.entity";
-import { RangeJadwalSeminar } from "./rangeJadwalSeminar.entity";
-import { Ruangan } from "./ruangan.entity";
-
-@Entity()
-export class Seminar {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
-
-  @ManyToOne(
-    () => RangeJadwalSeminar,
-    (rangeJadwalSeminar) => rangeJadwalSeminar.id,
-  )
-  rangeJadwal: RangeJadwalSeminar;
-
-  @ManyToOne(() => Ruangan, (ruangan) => ruangan.id)
-  ruangan: Ruangan;
-}
diff --git a/src/entities/sidang.entity.ts b/src/entities/sidang.entity.ts
deleted file mode 100644
index c2dc57ecdca72af17a7496883e74a716f46dbf1c..0000000000000000000000000000000000000000
--- a/src/entities/sidang.entity.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Pengguna } from "./pengguna.entity";
-import { RangeJadwalSidang } from "./rangeJadwalSidang.entity";
-import { Ruangan } from "./ruangan.entity";
-
-@Entity()
-export class Sidang {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
-
-  @ManyToOne(
-    () => RangeJadwalSidang,
-    (rangeJadwalSidang) => rangeJadwalSidang.id,
-  )
-  rangeJadwal: RangeJadwalSidang;
-
-  @ManyToOne(() => Ruangan, (ruangan) => ruangan.id)
-  ruangan: Ruangan;
-}
diff --git a/src/entities/topik.entity.ts b/src/entities/topik.entity.ts
index e0da355499ef58e1592a7b2b2899d27201c7bb50..75bbc9703a7c3c994c434b72b9b392a047ff6300 100644
--- a/src/entities/topik.entity.ts
+++ b/src/entities/topik.entity.ts
@@ -1,14 +1,37 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
+import {
+  Column,
+  Entity,
+  JoinColumn,
+  ManyToOne,
+  PrimaryGeneratedColumn,
+} from "typeorm";
 import { Pengguna } from "./pengguna.entity";
 
 @Entity()
 export class Topik {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
+  @ApiProperty()
   @Column()
   judul: string;
 
+  @ApiProperty()
+  @Column({ type: "text" })
+  deskripsi: string;
+
+  @ApiProperty({ type: () => Pengguna })
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  dosen: Pengguna;
+  @JoinColumn({ name: "idPengaju" })
+  pengaju: Pengguna;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @Column({ nullable: true })
+  idPengaju: string;
+
+  @ApiHideProperty()
+  @Column({ type: "boolean", default: true })
+  aktif: boolean;
 }
diff --git a/src/entities/tugas.entity.ts b/src/entities/tugas.entity.ts
deleted file mode 100644
index c445104cee8dbc1688e474e84031487e8d35f385..0000000000000000000000000000000000000000
--- a/src/entities/tugas.entity.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Kelas } from "./kelas.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class Tugas {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Kelas, (kelas) => kelas.id)
-  kelas: Kelas;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
-}
diff --git a/src/env.validation.ts b/src/env.validation.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1d5e6d037f02d9f54e2c3c7a0af08adc63785ff6
--- /dev/null
+++ b/src/env.validation.ts
@@ -0,0 +1,53 @@
+import { plainToInstance } from "class-transformer";
+import {
+  IsNumber,
+  validateSync,
+  Min,
+  Max,
+  IsString,
+  IsUrl,
+} from "class-validator";
+
+class EnvironmentVariables {
+  @IsString()
+  POSTGRES_HOST: string;
+
+  @IsNumber()
+  @Min(0)
+  @Max(65535)
+  POSTGRES_PORT: number;
+
+  @IsString()
+  POSTGRES_USER: string;
+
+  @IsString()
+  POSTGRES_PASSWORD: string;
+
+  @IsString()
+  POSTGRES_DATABASE: string;
+
+  @IsString()
+  @IsUrl({ require_tld: false })
+  AUTH_SERVICE_URL: string;
+
+  @IsString()
+  @IsUrl({ require_tld: false })
+  FE_URL: string;
+
+  @IsString()
+  COOKIE_NAME: string;
+}
+
+export function validate(config: Record<string, unknown>) {
+  const validatedConfig = plainToInstance(EnvironmentVariables, config, {
+    enableImplicitConversion: true,
+  });
+  const errors = validateSync(validatedConfig, {
+    skipMissingProperties: false,
+  });
+
+  if (errors.length > 0) {
+    throw new Error(errors.toString());
+  }
+  return validatedConfig;
+}
diff --git a/src/helper/roles.ts b/src/helper/roles.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ebd5385ec59c130f5baf20939e9d5162ba647136
--- /dev/null
+++ b/src/helper/roles.ts
@@ -0,0 +1,12 @@
+import { RoleEnum } from "src/entities/pengguna.entity";
+
+export const HIGH_AUTHORITY_ROLES = [RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS];
+export const DOSEN = [RoleEnum.S2_PEMBIMBING, RoleEnum.S2_PENGUJI];
+
+export function isHighAuthority(roles: RoleEnum[]) {
+  return roles.some((role) => HIGH_AUTHORITY_ROLES.includes(role));
+}
+
+export function isDosen(roles: RoleEnum[]) {
+  return roles.some((role) => DOSEN.includes(role));
+}
diff --git a/src/helper/sorting.ts b/src/helper/sorting.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1d171096c4e9d476f1e4d0349a762ecb158e4a07
--- /dev/null
+++ b/src/helper/sorting.ts
@@ -0,0 +1,11 @@
+import { OrderByCondition } from "typeorm";
+
+export function generateQueryBuilderOrderByObj(
+  keyToColumnMapping: Record<string, string>,
+  key: string,
+  sort?: "ASC" | "DESC",
+): OrderByCondition {
+  const orderByObj = {};
+  orderByObj[keyToColumnMapping[key]] = sort ?? "DESC";
+  return orderByObj;
+}
diff --git a/src/helper/validation.ts b/src/helper/validation.ts
deleted file mode 100644
index cfc65eb722c4859bd1569749bccb0e8785721b77..0000000000000000000000000000000000000000
--- a/src/helper/validation.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { NotFoundException } from "@nestjs/common";
-import { validate as uuidValidate } from "uuid";
-
-interface ID {
-  id: string;
-  object: string;
-}
-
-export function validateId(items: ID[]) {
-  for (const item of items) {
-    const isValidUUID = uuidValidate(item.id);
-
-    if (!isValidUUID) {
-      throw new NotFoundException(`${item.object} not found.`);
-    }
-  }
-}
diff --git a/src/konfigurasi/konfigurasi.controller.ts b/src/konfigurasi/konfigurasi.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..caf6a97a7950a431351f9d130913c7be03783420
--- /dev/null
+++ b/src/konfigurasi/konfigurasi.controller.ts
@@ -0,0 +1,37 @@
+import { Body, Controller, Get, Put, UseGuards } from "@nestjs/common";
+import { KonfigurasiService } from "./konfigurasi.service";
+import { KonfigurasiArrDto, UpdateKonfigurasiResDto } from "./konfigurasi.dto";
+import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
+import { RolesGuard } from "src/middlewares/roles.guard";
+import { RoleEnum } from "src/entities/pengguna.entity";
+import { Roles } from "src/middlewares/roles.decorator";
+import {
+  ApiBearerAuth,
+  ApiCookieAuth,
+  ApiOkResponse,
+  ApiTags,
+} from "@nestjs/swagger";
+
+@ApiTags("Konfigurasi")
+@ApiCookieAuth()
+@ApiBearerAuth()
+@Controller("konfigurasi")
+@UseGuards(CustomAuthGuard, RolesGuard)
+@Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+export class KonfigurasiController {
+  constructor(private readonly konfigurasiService: KonfigurasiService) {}
+
+  @ApiOkResponse({ type: UpdateKonfigurasiResDto })
+  @Put()
+  async updateKonfigurasi(
+    @Body() data: KonfigurasiArrDto,
+  ): Promise<UpdateKonfigurasiResDto> {
+    return await this.konfigurasiService.updateKonfigurasi(data);
+  }
+
+  @ApiOkResponse({ type: KonfigurasiArrDto })
+  @Get()
+  async getKonfigurasi(): Promise<KonfigurasiArrDto> {
+    return this.konfigurasiService.getKonfigurasi();
+  }
+}
diff --git a/src/konfigurasi/konfigurasi.dto.ts b/src/konfigurasi/konfigurasi.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1ebdd10da2f421f1aabdd53afe02b26466269032
--- /dev/null
+++ b/src/konfigurasi/konfigurasi.dto.ts
@@ -0,0 +1,19 @@
+import { ApiProperty } from "@nestjs/swagger";
+import { Type } from "class-transformer";
+import { ValidateNested } from "class-validator";
+import {
+  Konfigurasi,
+  KonfigurasiKeyEnum,
+} from "src/entities/konfigurasi.entity";
+
+export class KonfigurasiArrDto {
+  @ApiProperty({ type: [Konfigurasi] })
+  @ValidateNested({ each: true })
+  @Type(() => Konfigurasi)
+  data: Konfigurasi[];
+}
+
+export class UpdateKonfigurasiResDto {
+  @ApiProperty({ enum: KonfigurasiKeyEnum, isArray: true })
+  keys: KonfigurasiKeyEnum[];
+}
diff --git a/src/konfigurasi/konfigurasi.module.ts b/src/konfigurasi/konfigurasi.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8a1fcc680131abd16772f7002a54c203572257be
--- /dev/null
+++ b/src/konfigurasi/konfigurasi.module.ts
@@ -0,0 +1,13 @@
+import { Module } from "@nestjs/common";
+import { KonfigurasiController } from "./konfigurasi.controller";
+import { KonfigurasiService } from "./konfigurasi.service";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { Konfigurasi } from "src/entities/konfigurasi.entity";
+
+@Module({
+  imports: [TypeOrmModule.forFeature([Konfigurasi])],
+  controllers: [KonfigurasiController],
+  providers: [KonfigurasiService],
+  exports: [KonfigurasiService],
+})
+export class KonfigurasiModule {}
diff --git a/src/konfigurasi/konfigurasi.service.ts b/src/konfigurasi/konfigurasi.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6702c21184cc55ec1124dc165b0abd80abf4ad43
--- /dev/null
+++ b/src/konfigurasi/konfigurasi.service.ts
@@ -0,0 +1,44 @@
+import { Injectable } from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import {
+  Konfigurasi,
+  KonfigurasiKeyEnum,
+} from "src/entities/konfigurasi.entity";
+import { Repository } from "typeorm";
+import { KonfigurasiArrDto, UpdateKonfigurasiResDto } from "./konfigurasi.dto";
+
+@Injectable()
+export class KonfigurasiService {
+  constructor(
+    @InjectRepository(Konfigurasi)
+    private konfigurasiRepository: Repository<Konfigurasi>,
+  ) {}
+
+  async updateKonfigurasi({
+    data,
+  }: KonfigurasiArrDto): Promise<UpdateKonfigurasiResDto> {
+    await this.konfigurasiRepository.upsert(data, ["key"]);
+    const res = {
+      keys: data.map((d) => d.key),
+    };
+    return res;
+  }
+
+  async getKonfigurasi(): Promise<KonfigurasiArrDto> {
+    const data = await this.konfigurasiRepository.find();
+
+    return { data };
+  }
+
+  async getKonfigurasiByKey(
+    key: KonfigurasiKeyEnum,
+  ): Promise<string | undefined> {
+    const data = await this.konfigurasiRepository.findOne({
+      where: {
+        key,
+      },
+    });
+
+    return data?.value;
+  }
+}
diff --git a/src/main.ts b/src/main.ts
index 2553930747b4e0e2f52ff0a42f7f34ff44f0c407..05e2e26f2ea78343514d74aec6917e2a357dad8f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,10 +1,39 @@
 import { NestFactory } from "@nestjs/core";
 import { AppModule } from "./app.module";
+import * as cookieParser from "cookie-parser";
+import { ValidationPipe } from "@nestjs/common";
+import { ForbiddenExceptionFilter } from "./middlewares/forbidden-exception.filter";
+import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
 
 async function bootstrap() {
   const app = await NestFactory.create(AppModule);
 
-  app.enableCors();
+  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
+  app.useGlobalFilters(new ForbiddenExceptionFilter());
+
+  app.use(cookieParser());
+  app.enableCors({
+    origin: process.env.FE_URL,
+    credentials: true,
+  });
+
+  const options = new DocumentBuilder()
+    .setTitle("GraduIT S2 API")
+    .setDescription("GraduIT API Documentation for S2 services")
+    .setVersion("1.0")
+    .addTag("Alokasi Topik")
+    .addTag("Bimbingan")
+    .addTag("Dashboard")
+    .addTag("Dosen Bimbingan")
+    .addTag("Dosen Penguji")
+    .addTag("Konfigurasi")
+    .addTag("Registrasi Tesis")
+    .addTag("Registrasi Sidang Seminar")
+    .addCookieAuth(process.env.COOKIE_NAME)
+    .addBearerAuth()
+    .build();
+  const document = SwaggerModule.createDocument(app, options);
+  SwaggerModule.setup("api-docs", app, document);
 
   await app.listen(3000);
 }
diff --git a/src/middlewares/custom-auth.guard.ts b/src/middlewares/custom-auth.guard.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0f43322bf540292bf6a667e30f790ae768c426f2
--- /dev/null
+++ b/src/middlewares/custom-auth.guard.ts
@@ -0,0 +1,5 @@
+import { Injectable } from "@nestjs/common";
+import { AuthGuard } from "@nestjs/passport";
+
+@Injectable()
+export class CustomAuthGuard extends AuthGuard("custom") {}
diff --git a/src/middlewares/custom.strategy.ts b/src/middlewares/custom.strategy.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7b1e54d2d394064f285c26dcdcb2472cc35f54ae
--- /dev/null
+++ b/src/middlewares/custom.strategy.ts
@@ -0,0 +1,26 @@
+import { Injectable } from "@nestjs/common";
+import { PassportStrategy } from "@nestjs/passport";
+import { Request } from "express";
+import { Strategy } from "passport-custom";
+import { AuthService } from "src/auth/auth.service";
+
+@Injectable()
+export class CustomStrategy extends PassportStrategy(Strategy, "custom") {
+  constructor(private authService: AuthService) {
+    super();
+  }
+
+  async validate(req: Request) {
+    let token = "";
+
+    if (req?.cookies?.[process.env.COOKIE_NAME]) {
+      token = req.cookies[process.env.COOKIE_NAME];
+    }
+
+    if (req.headers?.authorization) {
+      token = req.headers.authorization.slice(7);
+    }
+
+    return this.authService.validate(token);
+  }
+}
diff --git a/src/middlewares/forbidden-exception.filter.ts b/src/middlewares/forbidden-exception.filter.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9d6de0d98638596e89d2c171d2c28dbbc145c205
--- /dev/null
+++ b/src/middlewares/forbidden-exception.filter.ts
@@ -0,0 +1,21 @@
+import {
+  ArgumentsHost,
+  Catch,
+  ExceptionFilter,
+  ForbiddenException,
+} from "@nestjs/common";
+import { Response } from "express";
+
+@Catch(ForbiddenException)
+export class ForbiddenExceptionFilter implements ExceptionFilter {
+  catch(exception: ForbiddenException, host: ArgumentsHost) {
+    const ctx = host.switchToHttp();
+    const response = ctx.getResponse<Response>();
+    const status = exception.getStatus();
+
+    response
+      .clearCookie(process.env.COOKIE_NAME)
+      .status(status)
+      .json(exception.getResponse());
+  }
+}
diff --git a/src/middlewares/roles.decorator.ts b/src/middlewares/roles.decorator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..27f28e876f3a010624b9e3d1c5fa7958845f1d1f
--- /dev/null
+++ b/src/middlewares/roles.decorator.ts
@@ -0,0 +1,5 @@
+import { SetMetadata } from "@nestjs/common";
+import { RoleEnum } from "src/entities/pengguna.entity";
+
+export const ROLES_KEY = "roles";
+export const Roles = (...roles: RoleEnum[]) => SetMetadata(ROLES_KEY, roles);
diff --git a/src/middlewares/roles.guard.ts b/src/middlewares/roles.guard.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9579c69121750015ae954ccf0c33809a2dd5c9b1
--- /dev/null
+++ b/src/middlewares/roles.guard.ts
@@ -0,0 +1,23 @@
+import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
+import { Reflector } from "@nestjs/core";
+import { RoleEnum } from "src/entities/pengguna.entity";
+import { ROLES_KEY } from "./roles.decorator";
+import { AuthDto } from "src/auth/auth.dto";
+
+@Injectable()
+export class RolesGuard implements CanActivate {
+  constructor(private reflector: Reflector) {}
+
+  async canActivate(context: ExecutionContext): Promise<boolean> {
+    const requiredRoles = this.reflector.getAllAndOverride<RoleEnum[]>(
+      ROLES_KEY,
+      [context.getHandler(), context.getClass()],
+    );
+
+    if (!requiredRoles) return true;
+
+    const { roles } = context.switchToHttp().getRequest().user as AuthDto;
+
+    return requiredRoles.some((role) => roles?.includes(role));
+  }
+}
diff --git a/src/pengguna/pengguna.module.ts b/src/pengguna/pengguna.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3e5276cbb4849fcc7453b7b2618834fcd3a2f3c7
--- /dev/null
+++ b/src/pengguna/pengguna.module.ts
@@ -0,0 +1,11 @@
+import { Module } from "@nestjs/common";
+import { PenggunaService } from "./pengguna.service";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { Pengguna } from "src/entities/pengguna.entity";
+
+@Module({
+  imports: [TypeOrmModule.forFeature([Pengguna])],
+  providers: [PenggunaService],
+  exports: [PenggunaService],
+})
+export class PenggunaModule {}
diff --git a/src/pengguna/pengguna.service.ts b/src/pengguna/pengguna.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..28281715431f789c616d7213ec8a8cea18b7c80c
--- /dev/null
+++ b/src/pengguna/pengguna.service.ts
@@ -0,0 +1,24 @@
+import { Injectable, NotFoundException } from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
+import { ArrayContains, Repository } from "typeorm";
+
+@Injectable()
+export class PenggunaService {
+  constructor(
+    @InjectRepository(Pengguna)
+    private penggunaRepo: Repository<Pengguna>,
+  ) {}
+
+  async isMahasiswaAktifOrFail(id: string) {
+    const mhs = await this.penggunaRepo.findOneBy({
+      id,
+      aktif: true,
+      roles: ArrayContains([RoleEnum.S2_MAHASISWA]),
+    });
+
+    if (!mhs) {
+      throw new NotFoundException("Mahasiswa aktif tidak ditemukan");
+    }
+  }
+}
diff --git a/src/registrasi-sidsem/registrasi-sidsem.controller.ts b/src/registrasi-sidsem/registrasi-sidsem.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e66b83522cabfb3d370827bf65798e1ca5fd0fd1
--- /dev/null
+++ b/src/registrasi-sidsem/registrasi-sidsem.controller.ts
@@ -0,0 +1,154 @@
+import {
+  Body,
+  Controller,
+  ForbiddenException,
+  Get,
+  Param,
+  Patch,
+  Post,
+  Query,
+  Req,
+  UseGuards,
+} from "@nestjs/common";
+import {
+  ApiBearerAuth,
+  ApiCookieAuth,
+  ApiCreatedResponse,
+  ApiOkResponse,
+  ApiOperation,
+  ApiTags,
+} from "@nestjs/swagger";
+import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
+import { Roles } from "src/middlewares/roles.decorator";
+import { RolesGuard } from "src/middlewares/roles.guard";
+import {
+  CreatePengajuanSidsemDto,
+  GetAllPengajuanSidangReqQueryDto,
+  GetAllPengajuanSidangRespDto,
+  GetOnePengajuanSidangRespDto,
+  PengajuanSidsemIdDto,
+  SidsemMhsIdParamDto,
+  UpdateSidsemDetailDto,
+  UpdateSidsemStatusDto,
+} from "./registrasi-sidsem.dto";
+import { RegistrasiSidsemService } from "./registrasi-sidsem.service";
+import { Request } from "express";
+import { AuthDto } from "src/auth/auth.dto";
+import { RoleEnum } from "src/entities/pengguna.entity";
+import {
+  DOSEN,
+  HIGH_AUTHORITY_ROLES,
+  isDosen,
+  isHighAuthority,
+} from "src/helper/roles";
+
+@ApiTags("Registrasi Sidang Seminar")
+@ApiBearerAuth()
+@ApiCookieAuth()
+@UseGuards(CustomAuthGuard, RolesGuard)
+@Controller("registrasi-sidsem")
+export class RegistrasiSidsemController {
+  constructor(private readonly regisSidsemService: RegistrasiSidsemService) {}
+
+  @ApiOperation({
+    summary: "Create new sidang seminar registration. Roles: S2_MAHASISWA",
+  })
+  @ApiCreatedResponse({ type: PengajuanSidsemIdDto })
+  @Roles(RoleEnum.S2_MAHASISWA)
+  @Post()
+  async create(@Req() req: Request, @Body() dto: CreatePengajuanSidsemDto) {
+    const { id } = req.user as AuthDto;
+    return this.regisSidsemService.create(id, dto);
+  }
+
+  @ApiOperation({
+    summary:
+      "Get all newest sidang seminar registration per mhs. Roles: ADMIN, S2_TIM_TESIS, S2_PEMBIMBING, S2_PENGUJI",
+  })
+  @ApiOkResponse({ type: GetAllPengajuanSidangRespDto })
+  @Roles(...HIGH_AUTHORITY_ROLES, ...DOSEN)
+  @Get()
+  async findAll(
+    @Req() req: Request,
+    @Query() query: GetAllPengajuanSidangReqQueryDto,
+  ) {
+    const { id, roles } = req.user as AuthDto;
+
+    if (!roles.includes(query.view)) {
+      throw new ForbiddenException();
+    }
+
+    return this.regisSidsemService.findAll(
+      query,
+      query.view === RoleEnum.S2_PEMBIMBING ? id : undefined,
+      query.view === RoleEnum.S2_PENGUJI ? id : undefined,
+    );
+  }
+
+  @ApiOperation({
+    summary:
+      "Get newest sidang seminar registration per mhs. Roles: ADMIN, S2_TIM_TESIS, S2_PEMBIMBING, S2_PENGUJI, S2_MAHASISWA",
+  })
+  @ApiOkResponse({ type: GetOnePengajuanSidangRespDto })
+  @Roles(...HIGH_AUTHORITY_ROLES, ...DOSEN, RoleEnum.S2_MAHASISWA)
+  @Get("/mahasiswa/:mhsId")
+  async findOne(@Req() req: Request, @Param() param: SidsemMhsIdParamDto) {
+    let idPenguji = undefined;
+    let idPembimbing = undefined;
+
+    const { roles, id } = req.user as AuthDto;
+
+    if (!isHighAuthority(roles)) {
+      if (roles.includes(RoleEnum.S2_PEMBIMBING)) {
+        idPembimbing = id;
+      }
+
+      if (roles.includes(RoleEnum.S2_PENGUJI)) {
+        idPenguji = id;
+      }
+
+      if (!isDosen(roles) && id !== param.mhsId) {
+        // user is mahasiswa
+        throw new ForbiddenException("Ini bukan data Anda.");
+      }
+    }
+    return this.regisSidsemService.findOne(
+      param.mhsId,
+      idPembimbing,
+      idPenguji,
+    );
+  }
+
+  @ApiOperation({
+    summary: "Update status sidang seminar. Roles: ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: PengajuanSidsemIdDto })
+  @Roles(...HIGH_AUTHORITY_ROLES)
+  @Patch("/mahasiswa/:mhsId/status")
+  async updateStatus(
+    @Param() param: SidsemMhsIdParamDto,
+    @Body() updateDto: UpdateSidsemStatusDto,
+    @Req() req: Request,
+  ) {
+    return this.regisSidsemService.updateStatus(
+      param.mhsId,
+      updateDto.status,
+      req,
+    );
+  }
+
+  @ApiOperation({
+    summary:
+      "Update detail of approved sidang seminar. Any falsify valued field will be ignored. Roles: ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: PengajuanSidsemIdDto })
+  @Roles(...HIGH_AUTHORITY_ROLES)
+  @Patch("/mahasiswa/:mhsId/detail")
+  async updateDetail(
+    @Param() param: SidsemMhsIdParamDto,
+    @Body() updateDto: UpdateSidsemDetailDto,
+    @Req() req: Request,
+  ) {
+    return this.regisSidsemService.updateDetail(param.mhsId, updateDto, req);
+  }
+}
diff --git a/src/registrasi-sidsem/registrasi-sidsem.dto.ts b/src/registrasi-sidsem/registrasi-sidsem.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fbead66ec4da85fd3d5ff7d5b537d1a8f873c361
--- /dev/null
+++ b/src/registrasi-sidsem/registrasi-sidsem.dto.ts
@@ -0,0 +1,177 @@
+import {
+  ApiProperty,
+  ApiPropertyOptional,
+  OmitType,
+  PickType,
+} from "@nestjs/swagger";
+import { Type } from "class-transformer";
+import {
+  ArrayNotEmpty,
+  IsDateString,
+  IsEnum,
+  IsNumberString,
+  IsOptional,
+  IsString,
+  IsUUID,
+  ValidateNested,
+} from "class-validator";
+import { BerkasSidsem } from "src/entities/berkasSidsem.entity";
+import {
+  PendaftaranSidsem,
+  SidsemStatus,
+  TipeSidsemEnum,
+} from "src/entities/pendaftaranSidsem";
+import { JalurEnum } from "src/entities/pendaftaranTesis.entity";
+import { RoleEnum } from "src/entities/pengguna.entity";
+
+export class SidsemViewQueryDto {
+  @IsEnum([
+    RoleEnum.S2_PEMBIMBING,
+    RoleEnum.ADMIN,
+    RoleEnum.S2_TIM_TESIS,
+    RoleEnum.S2_PENGUJI,
+  ])
+  @ApiProperty({
+    enum: [
+      RoleEnum.S2_PEMBIMBING,
+      RoleEnum.ADMIN,
+      RoleEnum.S2_TIM_TESIS,
+      RoleEnum.S2_PENGUJI,
+    ],
+  })
+  view:
+    | RoleEnum.S2_PEMBIMBING
+    | RoleEnum.ADMIN
+    | RoleEnum.S2_TIM_TESIS
+    | RoleEnum.S2_PENGUJI;
+}
+
+export class GetAllPengajuanSidangReqQueryDto extends SidsemViewQueryDto {
+  @ApiPropertyOptional()
+  @IsString()
+  @IsOptional()
+  search?: string;
+
+  @ApiPropertyOptional({ enum: TipeSidsemEnum })
+  @IsEnum(TipeSidsemEnum)
+  @IsOptional()
+  jenisSidang?: TipeSidsemEnum;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 1" })
+  page?: number;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: no limit" })
+  limit?: number;
+}
+
+export class GetAllPengajuanSidangItemDto {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  idPengajuanSidsem: string;
+
+  @ApiProperty()
+  idMahasiswa: string;
+
+  @ApiProperty()
+  nimMahasiswa: string;
+
+  @ApiProperty()
+  namaMahasiswa: string;
+
+  @ApiProperty({ nullable: true })
+  jadwalSidang: string | null;
+
+  @ApiProperty({ enum: TipeSidsemEnum })
+  jenisSidang: TipeSidsemEnum;
+
+  @ApiProperty({ nullable: true })
+  ruangan: string | null;
+
+  @ApiProperty({ enum: SidsemStatus })
+  status: SidsemStatus;
+
+  @ApiProperty({ type: [String] })
+  dosenPembimbing: string[];
+
+  @ApiProperty({ type: [BerkasSidsem] })
+  berkasSidsem: BerkasSidsem[];
+}
+
+export class GetAllPengajuanSidangRespDto {
+  @ApiProperty()
+  total: number;
+
+  @ApiProperty({ type: GetAllPengajuanSidangItemDto, isArray: true })
+  data: GetAllPengajuanSidangItemDto[];
+}
+
+export class GetOnePengajuanSidangRespDto extends GetAllPengajuanSidangItemDto {
+  @ApiProperty()
+  emailMahasiswa: string;
+  @ApiProperty({ enum: JalurEnum })
+  jalurPilihan: JalurEnum;
+  @ApiProperty()
+  judulTopik: string;
+  @ApiProperty()
+  deskripsiTopik: string;
+  @ApiProperty({ type: [String] })
+  dosenPembimbing: string[];
+  @ApiProperty({ type: [String] })
+  dosenPenguji: string[];
+
+  @ApiProperty()
+  judulSidsem: string;
+
+  @ApiProperty()
+  deskripsiSidsem: string;
+}
+
+class BerkasSidsemWithoutId extends OmitType(BerkasSidsem, ["id"] as const) {}
+
+export class CreatePengajuanSidsemDto extends PickType(PendaftaranSidsem, [
+  "judulSidsem",
+  "deskripsiSidsem",
+  "tipe",
+]) {
+  @ApiProperty({ type: [BerkasSidsemWithoutId] })
+  @ValidateNested({ each: true })
+  @ArrayNotEmpty()
+  @Type(() => BerkasSidsemWithoutId)
+  berkasSidsem: BerkasSidsemWithoutId[];
+}
+
+export class PengajuanSidsemIdDto extends PickType(PendaftaranSidsem, [
+  "id",
+] as const) {}
+
+export class UpdateSidsemDetailDto {
+  @ApiPropertyOptional()
+  @IsOptional()
+  @IsString()
+  ruangan?: string;
+
+  @ApiPropertyOptional()
+  @IsOptional()
+  @IsDateString()
+  jadwal?: Date;
+
+  @ApiPropertyOptional()
+  @IsOptional()
+  @IsUUID("all", { each: true })
+  dosenPengujiIds?: string[];
+}
+
+export class SidsemMhsIdParamDto {
+  @IsUUID()
+  @ApiProperty()
+  mhsId: string;
+}
+
+export class UpdateSidsemStatusDto {
+  @IsEnum([SidsemStatus.APPROVED, SidsemStatus.REJECTED])
+  @ApiProperty({ enum: [SidsemStatus.APPROVED, SidsemStatus.REJECTED] })
+  status: SidsemStatus.APPROVED | SidsemStatus.REJECTED;
+}
diff --git a/src/registrasi-sidsem/registrasi-sidsem.module.ts b/src/registrasi-sidsem/registrasi-sidsem.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db10743fedc136a73bebabfa779804b49133f606
--- /dev/null
+++ b/src/registrasi-sidsem/registrasi-sidsem.module.ts
@@ -0,0 +1,48 @@
+import { Module } from "@nestjs/common";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { AuthModule } from "src/auth/auth.module";
+import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
+import { PendaftaranSidsem } from "src/entities/pendaftaranSidsem";
+import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { CustomStrategy } from "src/middlewares/custom.strategy";
+import { RegistrasiSidsemController } from "./registrasi-sidsem.controller";
+import { RegistrasiSidsemService } from "./registrasi-sidsem.service";
+import { RegistrasiTesisService } from "src/registrasi-tesis/registrasi-tesis.service";
+import { RegistrasiTesisModule } from "src/registrasi-tesis/registrasi-tesis.module";
+import { BerkasSidsem } from "src/entities/berkasSidsem.entity";
+import { Topik } from "src/entities/topik.entity";
+import { PenggunaModule } from "src/pengguna/pengguna.module";
+import { PengujiSidsem } from "src/entities/pengujiSidsem.entity";
+import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module";
+import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
+import { Konfigurasi } from "src/entities/konfigurasi.entity";
+import { HttpModule } from "@nestjs/axios";
+
+@Module({
+  imports: [
+    TypeOrmModule.forFeature([
+      PendaftaranSidsem,
+      DosenBimbingan,
+      PendaftaranTesis,
+      Pengguna,
+      BerkasSidsem,
+      Topik,
+      PengujiSidsem,
+      Konfigurasi,
+    ]),
+    AuthModule,
+    RegistrasiTesisModule,
+    PenggunaModule,
+    KonfigurasiModule,
+    HttpModule,
+  ],
+  controllers: [RegistrasiSidsemController],
+  providers: [
+    RegistrasiSidsemService,
+    CustomStrategy,
+    RegistrasiTesisService,
+    KonfigurasiService,
+  ],
+})
+export class RegistrasiSidsemModule {}
diff --git a/src/registrasi-sidsem/registrasi-sidsem.service.ts b/src/registrasi-sidsem/registrasi-sidsem.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1f1b73a3907008766d8dfb9f34917b29283242e0
--- /dev/null
+++ b/src/registrasi-sidsem/registrasi-sidsem.service.ts
@@ -0,0 +1,529 @@
+import {
+  BadRequestException,
+  ForbiddenException,
+  Injectable,
+  InternalServerErrorException,
+  NotFoundException,
+} from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import {
+  cmpTipeSidsem,
+  PendaftaranSidsem,
+  SidsemStatus,
+  TipeSidsemEnum,
+} from "src/entities/pendaftaranSidsem";
+import { PengujiSidsem } from "src/entities/pengujiSidsem.entity";
+import { DataSource, In, Repository, Brackets } from "typeorm";
+import {
+  CreatePengajuanSidsemDto,
+  GetAllPengajuanSidangItemDto,
+  GetAllPengajuanSidangReqQueryDto,
+  GetAllPengajuanSidangRespDto,
+  GetOnePengajuanSidangRespDto,
+  PengajuanSidsemIdDto,
+  UpdateSidsemDetailDto,
+} from "./registrasi-sidsem.dto";
+import { RegStatus } from "src/entities/pendaftaranTesis.entity";
+import { RegistrasiTesisService } from "src/registrasi-tesis/registrasi-tesis.service";
+import { BerkasSidsem } from "src/entities/berkasSidsem.entity";
+import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
+import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
+import { KonfigurasiKeyEnum } from "src/entities/konfigurasi.entity";
+import * as dayjs from "dayjs";
+import { HttpService } from "@nestjs/axios";
+import { Request } from "express";
+import { firstValueFrom } from "rxjs";
+
+@Injectable()
+export class RegistrasiSidsemService {
+  constructor(
+    @InjectRepository(PendaftaranSidsem)
+    private pendaftaranSidsemRepo: Repository<PendaftaranSidsem>,
+    @InjectRepository(PengujiSidsem)
+    private pengujiSidsemRepo: Repository<PengujiSidsem>,
+    @InjectRepository(Pengguna)
+    private penggunaRepo: Repository<Pengguna>,
+    @InjectRepository(BerkasSidsem)
+    private berkasSidsemRepo: Repository<BerkasSidsem>,
+    private regTesisService: RegistrasiTesisService,
+    private dataSource: DataSource,
+    private konfService: KonfigurasiService,
+    private httpService: HttpService,
+  ) {}
+
+  private async getLatestPendaftaranSidsem(mhsId: string) {
+    return await this.pendaftaranSidsemRepo
+      .createQueryBuilder("ps")
+      .select([
+        "ps.id",
+        "ps.tipe",
+        "ps.jadwal",
+        "ps.ruangan",
+        "ps.status",
+        "ps.judulSidsem",
+        "ps.deskripsiSidsem",
+        "berkasSidsem",
+        "pt.id",
+        "pt.jalurPilihan",
+        "mahasiswa.id",
+        "mahasiswa.nim",
+        "mahasiswa.nama",
+        "mahasiswa.email",
+        "topik.judul",
+        "topik.deskripsi",
+        "dosenBimbingan.id",
+        "dosen.id",
+        "dosen.nama",
+        "penguji.id",
+        "dosenPenguji.id",
+        "dosenPenguji.nama",
+      ])
+      .leftJoin("ps.penguji", "penguji")
+      .leftJoin("ps.berkasSidsem", "berkasSidsem")
+      .leftJoin("penguji.dosen", "dosenPenguji")
+      .leftJoin("ps.pendaftaranTesis", "pt")
+      .leftJoin("pt.mahasiswa", "mahasiswa")
+      .leftJoin("pt.topik", "topik")
+      .leftJoin("pt.dosenBimbingan", "dosenBimbingan")
+      .leftJoin("dosenBimbingan.dosen", "dosen")
+      .where("pt.mahasiswaId = :mhsId", { mhsId })
+      .andWhere("mahasiswa.aktif = true")
+      .orderBy("ps.waktuPengiriman", "DESC")
+      .getOne();
+  }
+
+  konfKeysMapping = {
+    [TipeSidsemEnum.SEMINAR_1]: {
+      start: KonfigurasiKeyEnum.AWAL_SEMPRO,
+      end: KonfigurasiKeyEnum.AKHIR_SEMPRO,
+    },
+    [TipeSidsemEnum.SEMINAR_2]: {
+      start: KonfigurasiKeyEnum.AWAL_SEM_TESIS,
+      end: KonfigurasiKeyEnum.AKHIR_SEM_TESIS,
+    },
+    [TipeSidsemEnum.SIDANG]: {
+      start: KonfigurasiKeyEnum.AWAL_SIDANG,
+      end: KonfigurasiKeyEnum.AKHIR_SIDANG,
+    },
+  };
+
+  private async getSidsemKonfOrFail(tipe: TipeSidsemEnum) {
+    const mapping = this.konfKeysMapping[tipe];
+    const [start, end] = await Promise.all([
+      this.konfService.getKonfigurasiByKey(mapping.start),
+      this.konfService.getKonfigurasiByKey(mapping.end),
+    ]);
+
+    if (!start || !end) {
+      throw new BadRequestException(
+        `Sidang seminar bertipe ${tipe} belum dikonfigurasi`,
+      );
+    }
+
+    return { start: new Date(start), end: new Date(end) };
+  }
+
+  async create(
+    mhsId: string,
+    dto: CreatePengajuanSidsemDto,
+  ): Promise<PengajuanSidsemIdDto> {
+    const { start, end } = await this.getSidsemKonfOrFail(dto.tipe);
+
+    if (
+      dayjs(new Date()).isBefore(dayjs(start).startOf("d")) ||
+      dayjs(new Date()).isAfter(dayjs(end).endOf("d"))
+    ) {
+      throw new BadRequestException(
+        "Sidang seminar belum dibuka atau sudah ditutup",
+      );
+    }
+
+    const regTesis = await this.regTesisService.getNewestRegByMhsOrFail(mhsId);
+
+    if (regTesis.status !== RegStatus.APPROVED) {
+      throw new BadRequestException(
+        "Mahasiswa belum diterima sebagai mahasiswa tesis.",
+      );
+    }
+
+    // Check if mahasiswa already has pending registration
+    const lastPendaftaran = await this.getLatestPendaftaranSidsem(mhsId);
+    if (lastPendaftaran) {
+      const delta = cmpTipeSidsem(dto.tipe, lastPendaftaran.tipe);
+
+      if (
+        (delta !== 0 && delta !== 1) ||
+        (delta === 0 && lastPendaftaran.status !== SidsemStatus.REJECTED) ||
+        (delta === 1 && lastPendaftaran.status !== SidsemStatus.APPROVED)
+      ) {
+        {
+          throw new BadRequestException("Tipe sidsem invalid");
+        }
+      }
+    } else {
+      if (dto.tipe !== TipeSidsemEnum.SEMINAR_1) {
+        throw new BadRequestException("Tipe sidsem invalid");
+      }
+    }
+
+    const berkasSidsem = dto.berkasSidsem.map((berkasSubmisiTugas) =>
+      this.berkasSidsemRepo.create(berkasSubmisiTugas),
+    );
+
+    // Create new registration
+    const createdRegistration = this.pendaftaranSidsemRepo.create({
+      ...dto,
+      pendaftaranTesis: regTesis,
+      berkasSidsem,
+    });
+
+    await this.pendaftaranSidsemRepo.save(createdRegistration);
+
+    return {
+      id: createdRegistration.id,
+    };
+  }
+
+  async findAll(
+    query: GetAllPengajuanSidangReqQueryDto,
+    idPembimbing?: string,
+    idPenguji?: string,
+  ): Promise<GetAllPengajuanSidangRespDto> {
+    const baseQuery = this.pendaftaranSidsemRepo
+      .createQueryBuilder("ps")
+      .select([
+        "ps.id",
+        "ps.tipe",
+        "ps.status",
+        "pt.id",
+        "mahasiswa.id",
+        "mahasiswa.nim",
+        "mahasiswa.nama",
+        "dosenBimbingan.id",
+        "dosen.id",
+        "dosen.nama",
+        "berkasSidsem",
+      ])
+      .innerJoinAndSelect(
+        (qb) =>
+          qb
+            .select([
+              "ps.pendaftaranTesisId AS latest_pendaftaranTesisId",
+              "MAX(ps.waktuPengiriman) AS latestPengiriman",
+            ])
+            .from(PendaftaranSidsem, "ps")
+            .groupBy("ps.pendaftaranTesisId"),
+        "latest",
+        "latest.latest_pendaftaranTesisId = ps.pendaftaranTesisId AND ps.waktuPengiriman = latest.latestPengiriman",
+      )
+      .leftJoin("ps.pendaftaranTesis", "pt")
+      .leftJoin("ps.berkasSidsem", "berkasSidsem")
+      .leftJoin("pt.dosenBimbingan", "dosenBimbingan")
+      .leftJoin("dosenBimbingan.dosen", "dosen")
+      .leftJoin("pt.mahasiswa", "mahasiswa")
+      .where("mahasiswa.aktif = true")
+      .orderBy("ps.waktuPengiriman", "DESC");
+
+    if (idPembimbing) {
+      baseQuery
+        .innerJoin("pt.dosenBimbingan", "dosenBimbinganFilter")
+        .andWhere("dosenBimbinganFilter.idDosen = :idPembimbing", {
+          idPembimbing,
+        });
+    }
+
+    if (idPenguji) {
+      baseQuery
+        .innerJoin("ps.penguji", "pengujiFilter")
+        .andWhere("pengujiFilter.idDosen = :idPenguji", {
+          idPenguji,
+        });
+    }
+
+    if (query.search) {
+      baseQuery.andWhere(
+        new Brackets((qb) =>
+          qb
+            .where("mahasiswa.nama ILIKE :search", {
+              search: `%${query.search}%`,
+            })
+            .orWhere("mahasiswa.nim ILIKE :search", {
+              search: `%${query.search}%`,
+            }),
+        ),
+      );
+    }
+
+    if (query.jenisSidang) {
+      baseQuery.andWhere("ps.jenisSidang = :jenisSidang", {
+        jenisSidang: query.jenisSidang,
+      });
+    }
+
+    if (query.limit) {
+      baseQuery.take(query.limit);
+      baseQuery.skip((query.page - 1) * query.limit);
+    }
+
+    const [queryData, total] = await baseQuery.getManyAndCount();
+
+    const data: GetAllPengajuanSidangItemDto[] = queryData.map((res) => ({
+      idPengajuanSidsem: res.id,
+      idMahasiswa: res.pendaftaranTesis.mahasiswa.id,
+      nimMahasiswa: res.pendaftaranTesis.mahasiswa.nim,
+      namaMahasiswa: res.pendaftaranTesis.mahasiswa.nama,
+      jadwalSidang: !!res.jadwal ? res.jadwal.toISOString() : null,
+      jenisSidang: res.tipe,
+      ruangan: res.ruangan,
+      status: res.status,
+      dosenPembimbing: res.pendaftaranTesis.dosenBimbingan.map(
+        (dosen) => dosen.dosen.nama,
+      ),
+      berkasSidsem: res.berkasSidsem,
+    }));
+
+    return { data, total };
+  }
+
+  async findOne(
+    mhsId: string,
+    idPembimbing?: string,
+    idPenguji?: string,
+  ): Promise<GetOnePengajuanSidangRespDto> {
+    const latest = await this.getLatestPendaftaranSidsem(mhsId);
+
+    if (!latest) {
+      throw new NotFoundException("Pendaftaran sidsem tidak ditemukan");
+    }
+
+    function isPembimbing() {
+      return latest.pendaftaranTesis.dosenBimbingan.some(
+        ({ dosen: { id } }) => id === idPembimbing,
+      );
+    }
+
+    function isPenguji() {
+      return latest.penguji.some(({ dosen: { id } }) => id === idPenguji);
+    }
+
+    if (idPembimbing && idPenguji) {
+      if (!isPembimbing() && !isPenguji()) {
+        throw new ForbiddenException(
+          "Anda tidak terdaftar sebagai pembimbing atau penguji",
+        );
+      }
+    } else if (idPembimbing) {
+      if (!isPembimbing()) {
+        throw new ForbiddenException("Anda tidak terdaftar sebagai pembimbing");
+      }
+    } else if (idPenguji) {
+      if (!isPenguji()) {
+        throw new ForbiddenException("Anda tidak terdaftar sebagai penguji");
+      }
+    }
+
+    const data: GetOnePengajuanSidangRespDto = {
+      idPengajuanSidsem: latest.id,
+      idMahasiswa: latest.pendaftaranTesis.mahasiswa.id,
+      nimMahasiswa: latest.pendaftaranTesis.mahasiswa.nim,
+      namaMahasiswa: latest.pendaftaranTesis.mahasiswa.nama,
+      emailMahasiswa: latest.pendaftaranTesis.mahasiswa.email,
+      jadwalSidang: latest.jadwal ? latest.jadwal.toISOString() : null,
+      jenisSidang: latest.tipe,
+      ruangan: latest.ruangan,
+      jalurPilihan: latest.pendaftaranTesis.jalurPilihan,
+      judulTopik: latest.pendaftaranTesis.topik.judul,
+      deskripsiTopik: latest.pendaftaranTesis.topik.deskripsi,
+      status: latest.status,
+      berkasSidsem: latest.berkasSidsem,
+      judulSidsem: latest.judulSidsem,
+      deskripsiSidsem: latest.deskripsiSidsem,
+      dosenPembimbing: latest.pendaftaranTesis.dosenBimbingan.map(
+        ({ dosen: { nama } }) => nama,
+      ),
+      dosenPenguji: latest.penguji.map(({ dosen: { nama } }) => nama),
+    };
+
+    return data;
+  }
+
+  async updateStatus(
+    mhsId: string,
+    status: SidsemStatus.REJECTED | SidsemStatus.APPROVED,
+    req: Request,
+  ): Promise<PengajuanSidsemIdDto> {
+    const latest = await this.getLatestPendaftaranSidsem(mhsId);
+
+    if (!latest || latest.status !== SidsemStatus.NOT_ASSIGNED) {
+      throw new BadRequestException(
+        "Pendaftaran sidsem yang pending tidak ditemukan",
+      );
+    }
+
+    let token = "";
+    if (req?.cookies?.[process.env.COOKIE_NAME]) {
+      token = req.cookies[process.env.COOKIE_NAME];
+    }
+    if (req.headers?.authorization) {
+      token = req.headers.authorization.slice(7);
+    }
+
+    const { data: notif } = await firstValueFrom(
+      this.httpService.post(
+        `${process.env.AUTH_SERVICE_URL}/notifikasi`,
+        {
+          title: `Pendaftaran ${latest.tipe.split("_").join(" ").toLowerCase()} Anda ${status === SidsemStatus.APPROVED ? "diterima" : "ditolak"}`,
+          description: `Pendaftaran tesis Anda ${status === SidsemStatus.APPROVED ? "diterima" : "ditolak"}. Silahkan periksa kembali data Anda untuk mengetahui lebih lanjut.`,
+          penggunaId: mhsId,
+        },
+        {
+          headers: {
+            Authorization: `Bearer ${token}`,
+          },
+        },
+      ),
+    );
+
+    try {
+      await this.pendaftaranSidsemRepo.update(latest.id, {
+        status,
+      });
+    } catch {
+      await firstValueFrom(
+        this.httpService.delete(
+          `${process.env.AUTH_SERVICE_URL}/notifikasi/${notif.id}`,
+          {
+            headers: {
+              Authorization: `Bearer ${token}`,
+            },
+          },
+        ),
+      );
+    }
+
+    return { id: latest.id } as PengajuanSidsemIdDto;
+  }
+
+  async updateDetail(
+    mhsId: string,
+    updateDto: UpdateSidsemDetailDto,
+    req: Request,
+  ): Promise<PengajuanSidsemIdDto> {
+    const latest = await this.getLatestPendaftaranSidsem(mhsId);
+
+    if (!latest || latest.status !== SidsemStatus.APPROVED) {
+      throw new BadRequestException(
+        "Pendaftaran sidsem yang disetujui tidak ditemukan",
+      );
+    }
+
+    if (updateDto.jadwal) {
+      if (dayjs(updateDto.jadwal).isBefore(dayjs(new Date()).endOf("d"))) {
+        throw new BadRequestException("Jadwal sidang tidak valid");
+      }
+    }
+
+    if (updateDto.dosenPengujiIds) {
+      const newPengujiList = await this.penggunaRepo.findBy({
+        id: In(updateDto.dosenPengujiIds),
+      });
+
+      if (
+        newPengujiList.length !== updateDto.dosenPengujiIds.length ||
+        newPengujiList.some(
+          (dosen) => !dosen.roles.includes(RoleEnum.S2_PENGUJI),
+        )
+      )
+        throw new BadRequestException(
+          "Dosen id list contains invalid user ids",
+        );
+
+      const currentPenguji = await this.pengujiSidsemRepo.findBy({
+        idSidsem: latest.id,
+      });
+
+      const newPengujiIds = newPengujiList.map((dosen) => dosen.id);
+      const currentPengujiIds = currentPenguji.map(
+        (currentPembimbing) => currentPembimbing.idDosen,
+      );
+
+      const idsToBeAdded = newPengujiIds.filter(
+        (newId) => !currentPengujiIds.includes(newId),
+      );
+
+      const idsToBeDeleted = currentPengujiIds.filter(
+        (newId) => !newPengujiIds.includes(newId),
+      );
+
+      const queryRunner = this.dataSource.createQueryRunner();
+      await queryRunner.connect();
+      await queryRunner.startTransaction();
+
+      let token = "";
+      if (req?.cookies?.[process.env.COOKIE_NAME]) {
+        token = req.cookies[process.env.COOKIE_NAME];
+      }
+      if (req.headers?.authorization) {
+        token = req.headers.authorization.slice(7);
+      }
+
+      const { data: notif } = await firstValueFrom(
+        this.httpService.post(
+          `${process.env.AUTH_SERVICE_URL}/notifikasi`,
+          {
+            title: `Detail pendaftaran ${latest.tipe.split("_").join(" ").toLowerCase()} Anda diubah`,
+            description: `Detail pendaftaran ${latest.tipe.split("_").join(" ").toLowerCase()} Anda diubah. Silahkan periksa kembali data Anda untuk mengetahui lebih lanjut.`,
+            penggunaId: mhsId,
+          },
+          {
+            headers: {
+              Authorization: `Bearer ${token}`,
+            },
+          },
+        ),
+      );
+
+      try {
+        await queryRunner.manager.insert(
+          PengujiSidsem,
+          idsToBeAdded.map((idDosen) => ({ sidsem: latest, idDosen })),
+        );
+        await queryRunner.manager.delete(PengujiSidsem, {
+          idDosen: In(idsToBeDeleted),
+        });
+
+        if (updateDto.ruangan || updateDto.jadwal) {
+          await queryRunner.manager.update(PendaftaranSidsem, latest.id, {
+            ruangan: updateDto.ruangan,
+            jadwal: updateDto.jadwal,
+          });
+        }
+
+        await queryRunner.commitTransaction();
+      } catch (err) {
+        await queryRunner.rollbackTransaction();
+
+        await firstValueFrom(
+          this.httpService.delete(
+            `${process.env.AUTH_SERVICE_URL}/notifikasi/${notif.id}`,
+            {
+              headers: {
+                Authorization: `Bearer ${token}`,
+              },
+            },
+          ),
+        );
+
+        throw new InternalServerErrorException();
+      } finally {
+        await queryRunner.release();
+      }
+    } else {
+      await this.pendaftaranSidsemRepo.update(latest.id, {
+        ...updateDto,
+      });
+    }
+
+    return { id: latest.id } as PengajuanSidsemIdDto;
+  }
+}
diff --git a/src/registrasi-tesis/registrasi-tesis.controller.ts b/src/registrasi-tesis/registrasi-tesis.controller.ts
index 18284cfb10c4eb44e3f38a2adc6b352d90696472..d7585fc17933028c4352b0d2f132a807b883a1a3 100644
--- a/src/registrasi-tesis/registrasi-tesis.controller.ts
+++ b/src/registrasi-tesis/registrasi-tesis.controller.ts
@@ -1,27 +1,270 @@
-import { Body, Controller, Post, Get, Param } from "@nestjs/common";
+import {
+  Body,
+  Controller,
+  ForbiddenException,
+  Get,
+  Param,
+  Patch,
+  Post,
+  Query,
+  Req,
+  UseGuards,
+} from "@nestjs/common";
+import {
+  ApiBadRequestResponse,
+  ApiBearerAuth,
+  ApiCookieAuth,
+  ApiCreatedResponse,
+  ApiNotFoundResponse,
+  ApiOkResponse,
+  ApiOperation,
+  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 {
+  FindAllNewestRegRespDto,
+  GetByIdRespDto,
+  IdDto,
+  RegByMhsParamDto,
+  RegDto,
+  RegQueryDto,
+  RegStatisticsRespDto,
+  UpdateByMhsParamsDto,
+  UpdateInterviewBodyDto,
+  UpdatePembimbingBodyDto,
+  UpdateStatusBodyDto,
+  ViewQueryDto,
+} from "./registrasi-tesis.dto";
 import { RegistrasiTesisService } from "./registrasi-tesis.service";
-import { RegistrasiTopikDto } from "src/dto/registrasi-topik";
 
+@ApiCookieAuth()
+@ApiBearerAuth()
+@ApiTags("Registrasi Tesis")
 @Controller("registrasi-tesis")
 export class RegistrasiTesisController {
   constructor(
     private readonly registrasiTesisService: RegistrasiTesisService,
   ) {}
 
-  // TODO: Protect using roles and guards
+  @ApiOperation({
+    summary: "Create new registration. Roles: S2_MAHASISWA",
+  })
+  @ApiCreatedResponse({ type: IdDto })
+  @ApiNotFoundResponse({ description: "Penerima atau topik tidak ditemukan" })
+  @ApiBadRequestResponse({
+    description:
+      "Mahasiswa sedang memiliki pendaftaran aktif atau judul dan deskripsi topik baru tidak ada",
+  })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.S2_MAHASISWA)
+  @Post()
+  async createTopicRegistration(
+    @Body() topicRegistrationDto: RegDto,
+    @Req() req: Request,
+  ): Promise<IdDto> {
+    const { id } = req.user as AuthDto;
 
+    return this.registrasiTesisService.createTopicRegistration(
+      id,
+      topicRegistrationDto,
+    );
+  }
+
+  @ApiOperation({
+    summary:
+      "Find registrations (historical) by Mahasiswa ID. Roles: S2_MAHASISWA, ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: [GetByIdRespDto] })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.S2_MAHASISWA, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
   @Get("/mahasiswa/:mahasiswaId")
-  findByUserId(@Param() params: { mahasiswaId: string }) {
-    return this.registrasiTesisService.findByUserId(params.mahasiswaId);
+  async findByUserId(@Param() params: RegByMhsParamDto, @Req() req: Request) {
+    const { id, roles } = req.user as AuthDto;
+
+    if (
+      !roles.includes(RoleEnum.ADMIN) &&
+      !roles.includes(RoleEnum.S2_TIM_TESIS)
+    ) {
+      // roles only include RoleEnum.S2_MAHASISWA
+      if (id !== params.mahasiswaId) {
+        throw new ForbiddenException();
+      }
+    }
+
+    return this.registrasiTesisService.findByUserId(
+      params.mahasiswaId,
+      false,
+      undefined,
+    );
   }
 
-  @Post()
-  async createTopicRegistration(
-    @Body() topicRegistrationDto: RegistrasiTopikDto,
+  @ApiOperation({
+    summary:
+      "Find newest registration by Mahasiswa ID. Roles: ADMIN, S2_TIM_TESIS, S2_PEMBIMBING",
+  })
+  @ApiOkResponse({ type: GetByIdRespDto })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING)
+  @Get("/mahasiswa/:mahasiswaId/newest")
+  async findNewestByUserId(
+    @Param() params: RegByMhsParamDto,
+    @Req() req: Request,
   ) {
-    return this.registrasiTesisService.createTopicRegistration(
-      "ae9697b9-590f-4820-826b-948f5e746ca7", // TODO: Get user id from request, for now use generated UUID
-      topicRegistrationDto,
+    const { id, roles } = req.user as AuthDto;
+
+    let idPenerima = undefined;
+    if (
+      !roles.includes(RoleEnum.ADMIN) &&
+      !roles.includes(RoleEnum.S2_TIM_TESIS)
+    ) {
+      // roles only include RoleEnum.S2_PEMBIMBING
+      idPenerima = id;
+    }
+
+    const res = await this.registrasiTesisService.findByUserId(
+      params.mahasiswaId,
+      true,
+      idPenerima,
+    );
+
+    return res[0];
+  }
+
+  @ApiOperation({
+    summary:
+      "Get statistics of registrations. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: RegStatisticsRespDto })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+  @Get("/statistics")
+  async getRegsStatistics(@Req() req: Request, @Query() query: ViewQueryDto) {
+    const { id: idPenerima, roles } = req.user as AuthDto;
+
+    if (!roles.includes(query.view)) {
+      throw new ForbiddenException();
+    }
+
+    return this.registrasiTesisService.getRegsStatistics({
+      idPenerima: query.view == RoleEnum.S2_PEMBIMBING ? idPenerima : undefined,
+    });
+  }
+
+  // Admin & TimTesis view will show newst reg records per Mahasiswa
+  // Pembimbing view will show all regs towards them
+  @ApiOperation({
+    summary:
+      "Find all newest registration for each Mahasiswa. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: FindAllNewestRegRespDto, isArray: true })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+  @Get()
+  async findAllNewest(
+    @Query()
+    query: RegQueryDto,
+    @Req() req: Request,
+  ) {
+    const { id: idPenerima, roles } = req.user as AuthDto;
+
+    if (!roles.includes(query.view)) {
+      throw new ForbiddenException();
+    }
+
+    return await this.registrasiTesisService.findAllRegs({
+      ...query,
+      page: query.page || 1,
+      idPenerima:
+        query.view === RoleEnum.S2_PEMBIMBING ? idPenerima : undefined,
+    });
+  }
+
+  @ApiOperation({
+    summary:
+      "Update interview date of newest in process registration by Mahasiswa ID. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: IdDto })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING)
+  @Patch("/:mhsId/interview")
+  async updateInterviewDateByMhsId(
+    @Param() params: UpdateByMhsParamsDto,
+    @Body() body: UpdateInterviewBodyDto,
+    @Req() req: Request,
+  ) {
+    const { id, roles } = req.user as AuthDto;
+    let idPenerima = undefined;
+
+    if (
+      !roles.includes(RoleEnum.ADMIN) &&
+      !roles.includes(RoleEnum.S2_TIM_TESIS)
+    ) {
+      // roles only include RoleEnum.S2_PEMBIMBING
+      idPenerima = id;
+    }
+
+    return await this.registrasiTesisService.updateInterviewDate(
+      params.mhsId,
+      body,
+      req,
+      idPenerima,
+    );
+  }
+
+  @ApiOperation({
+    summary:
+      "Update status of newest registration by Mahasiswa ID. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: IdDto })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING)
+  @Patch("/:mhsId/status")
+  async updateStatusByMhsId(
+    @Param() params: UpdateByMhsParamsDto,
+    @Body() body: UpdateStatusBodyDto,
+    @Req() req: Request,
+  ) {
+    const { id, roles } = req.user as AuthDto;
+    let idPenerima = undefined;
+
+    if (
+      !roles.includes(RoleEnum.ADMIN) &&
+      !roles.includes(RoleEnum.S2_TIM_TESIS)
+    ) {
+      // roles only include RoleEnum.S2_PEMBIMBING
+      idPenerima = id;
+    }
+
+    return await this.registrasiTesisService.updateStatus(
+      params.mhsId,
+      body,
+      req,
+      idPenerima,
+    );
+  }
+
+  @ApiOperation({
+    summary:
+      "Update pembimbing list of approved registration by Mahasiswa ID. Roles: ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: IdDto })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+  @Patch("/:mhsId/pembimbing")
+  async updatePembimbingListByMhsId(
+    @Param() params: UpdateByMhsParamsDto,
+    @Body() body: UpdatePembimbingBodyDto,
+    @Req() req: Request,
+  ) {
+    return await this.registrasiTesisService.updatePembimbingList(
+      params.mhsId,
+      body,
+      req,
     );
   }
 }
diff --git a/src/registrasi-tesis/registrasi-tesis.dto.ts b/src/registrasi-tesis/registrasi-tesis.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7d5d1bf284f8957d52f75af71505fad198e74b52
--- /dev/null
+++ b/src/registrasi-tesis/registrasi-tesis.dto.ts
@@ -0,0 +1,196 @@
+import {
+  IsDateString,
+  IsEnum,
+  IsNumberString,
+  IsOptional,
+  IsString,
+  IsUUID,
+} from "@nestjs/class-validator";
+import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger";
+import { ArrayMinSize, ArrayUnique, IsArray } from "class-validator";
+import {
+  JalurEnum,
+  PendaftaranTesis,
+  RegStatus,
+} from "src/entities/pendaftaranTesis.entity";
+import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
+
+export class RegDto {
+  @IsUUID()
+  @ApiProperty()
+  idPenerima: string;
+
+  @IsUUID()
+  @IsOptional()
+  @ApiPropertyOptional()
+  idTopik?: string;
+
+  @IsEnum(JalurEnum)
+  @ApiProperty({ enum: JalurEnum })
+  jalurPilihan: JalurEnum;
+
+  @IsString()
+  @IsOptional()
+  @ApiPropertyOptional()
+  judulTopik?: string;
+
+  @IsString()
+  @IsOptional()
+  @ApiPropertyOptional()
+  deskripsiTopik?: string;
+}
+
+export class RegByMhsParamDto {
+  @IsUUID()
+  @ApiProperty()
+  mahasiswaId: string;
+}
+
+export class IdDto {
+  @IsUUID()
+  @ApiProperty()
+  id: string;
+}
+
+export class ViewQueryDto {
+  @IsEnum([RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS])
+  @ApiProperty({
+    enum: [RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS],
+  })
+  view: RoleEnum.S2_PEMBIMBING | RoleEnum.ADMIN | RoleEnum.S2_TIM_TESIS;
+}
+
+export class RegQueryDto extends ViewQueryDto {
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional()
+  page?: number;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional()
+  limit?: number;
+
+  @IsOptional()
+  @IsString()
+  @ApiPropertyOptional()
+  search?: string;
+
+  @IsOptional()
+  @IsEnum(RegStatus)
+  @ApiPropertyOptional({ enum: RegStatus })
+  status?: RegStatus;
+
+  @IsOptional()
+  @IsEnum(["nim"])
+  @ApiPropertyOptional({ enum: ["nim"] })
+  order_by?: "nim";
+
+  @IsOptional()
+  @IsEnum(["ASC", "DESC"])
+  @ApiPropertyOptional({ enum: ["ASC", "DESC"] })
+  sort?: "ASC" | "DESC";
+}
+
+export class FindAllNewestRegRespDataDto {
+  @ApiProperty()
+  pendaftaran_id: string;
+
+  @ApiProperty()
+  nim: string;
+
+  @ApiProperty()
+  mahasiswa_nama: string;
+
+  @ApiProperty()
+  mahasiswa_id: string;
+
+  @ApiProperty()
+  pembimbing_nama: string;
+
+  @ApiProperty()
+  status: string;
+
+  @ApiProperty()
+  jadwal_interview: Date;
+}
+
+export class FindAllNewestRegRespDto {
+  @ApiProperty({ type: [FindAllNewestRegRespDataDto] })
+  data: FindAllNewestRegRespDataDto[];
+
+  @ApiProperty()
+  count: number;
+}
+
+export class RegStatisticsRespDataDto {
+  @ApiProperty()
+  amount: number;
+  @ApiProperty()
+  percentage: number;
+}
+
+export class RegStatisticsRespDto {
+  @ApiProperty({ type: RegStatisticsRespDataDto })
+  diterima: RegStatisticsRespDataDto;
+
+  @ApiProperty({ type: RegStatisticsRespDataDto })
+  sedang_proses: RegStatisticsRespDataDto;
+
+  @ApiProperty({ type: RegStatisticsRespDataDto })
+  ditolak: RegStatisticsRespDataDto;
+}
+
+export class UpdateByMhsParamsDto {
+  @IsUUID()
+  @ApiProperty()
+  mhsId: string;
+}
+
+export class UpdateInterviewBodyDto {
+  @ApiProperty({ type: Date })
+  @IsDateString()
+  date: string;
+}
+
+export class UpdateStatusBodyDto {
+  @ApiProperty({ enum: [RegStatus.APPROVED, RegStatus.REJECTED] })
+  @IsEnum([RegStatus.APPROVED, RegStatus.REJECTED])
+  status: RegStatus;
+}
+
+export class UpdatePembimbingBodyDto {
+  @ApiProperty({ type: [String] })
+  @IsArray()
+  @IsUUID("all", { each: true })
+  @ArrayMinSize(1)
+  @ArrayUnique()
+  pembimbing_ids: string[];
+}
+
+class DosenPembimbingDto extends PickType(Pengguna, [
+  "id",
+  "nama",
+  "kontakWhatsApp",
+  "kontakMsTeams",
+  "kontakEmail",
+  "kontakTelp",
+  "kontakLainnya",
+] as const) {}
+
+export class GetByIdRespDto extends PickType(PendaftaranTesis, [
+  "id",
+  "jadwalInterview",
+  "status",
+  "jalurPilihan",
+  "waktuPengiriman",
+] as const) {
+  @ApiProperty()
+  judulTopik: string;
+
+  @ApiProperty()
+  deskripsiTopik: string;
+
+  @ApiProperty({ type: [DosenPembimbingDto] })
+  dosenPembimbing: DosenPembimbingDto[];
+}
diff --git a/src/registrasi-tesis/registrasi-tesis.module.ts b/src/registrasi-tesis/registrasi-tesis.module.ts
index 9b7a365ee001666b6b95db5168708a0b40bc9063..7c4c8a408b9f2d910ecb11cd35ec1182885bb787 100644
--- a/src/registrasi-tesis/registrasi-tesis.module.ts
+++ b/src/registrasi-tesis/registrasi-tesis.module.ts
@@ -1,22 +1,31 @@
 import { Module } from "@nestjs/common";
-import { PengajuanPengambilanTopik } from "src/entities/pengajuanPengambilanTopik.entity";
+import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity";
 import { Pengguna } from "src/entities/pengguna.entity";
 import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
 import { TypeOrmModule } from "@nestjs/typeorm";
 import { RegistrasiTesisController } from "./registrasi-tesis.controller";
 import { RegistrasiTesisService } from "./registrasi-tesis.service";
 import { Topik } from "src/entities/topik.entity";
+import { CustomStrategy } from "src/middlewares/custom.strategy";
+import { AuthModule } from "src/auth/auth.module";
+import { PenggunaModule } from "src/pengguna/pengguna.module";
+import { PenggunaService } from "src/pengguna/pengguna.service";
+import { HttpModule } from "@nestjs/axios";
 
 @Module({
   imports: [
     TypeOrmModule.forFeature([
       Pengguna,
       DosenBimbingan,
-      PengajuanPengambilanTopik,
+      PendaftaranTesis,
       Topik,
     ]),
+    AuthModule,
+    PenggunaModule,
+    HttpModule,
   ],
   controllers: [RegistrasiTesisController],
-  providers: [RegistrasiTesisService],
+  providers: [RegistrasiTesisService, CustomStrategy, PenggunaService],
+  exports: [RegistrasiTesisService],
 })
 export class RegistrasiTesisModule {}
diff --git a/src/registrasi-tesis/registrasi-tesis.service.ts b/src/registrasi-tesis/registrasi-tesis.service.ts
index 39ca645e009b0ff800338d6303308b9234b1c6a1..18135e28d03c83d6c97926c377218a43dce95180 100644
--- a/src/registrasi-tesis/registrasi-tesis.service.ts
+++ b/src/registrasi-tesis/registrasi-tesis.service.ts
@@ -1,76 +1,703 @@
-import { Injectable, NotFoundException } from "@nestjs/common";
+import {
+  BadRequestException,
+  ForbiddenException,
+  Injectable,
+  InternalServerErrorException,
+  NotFoundException,
+} from "@nestjs/common";
 import { InjectRepository } from "@nestjs/typeorm";
-import { Repository } from "typeorm";
-import { PengajuanPengambilanTopik } from "src/entities/pengajuanPengambilanTopik.entity";
-import { RegistrasiTopikDto } from "src/dto/registrasi-topik";
-import { Pengguna } from "src/entities/pengguna.entity";
-import { validateId } from "src/helper/validation";
+import * as dayjs from "dayjs";
+import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
+import {
+  PendaftaranTesis,
+  RegStatus,
+} from "src/entities/pendaftaranTesis.entity";
+import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
 import { Topik } from "src/entities/topik.entity";
+import { generateQueryBuilderOrderByObj } from "src/helper/sorting";
+import { ArrayContains, Brackets, DataSource, In, Repository } from "typeorm";
+import {
+  FindAllNewestRegRespDto,
+  IdDto,
+  RegDto,
+  RegStatisticsRespDto,
+  UpdateInterviewBodyDto,
+  UpdatePembimbingBodyDto,
+  UpdateStatusBodyDto,
+} from "./registrasi-tesis.dto";
+import { PenggunaService } from "src/pengguna/pengguna.service";
+import { HttpService } from "@nestjs/axios";
+import { Request } from "express";
+import { firstValueFrom } from "rxjs";
 
 @Injectable()
 export class RegistrasiTesisService {
   constructor(
-    @InjectRepository(PengajuanPengambilanTopik)
-    private pengajuanPengambilanTopikRepository: Repository<PengajuanPengambilanTopik>,
+    @InjectRepository(PendaftaranTesis)
+    private pendaftaranTesisRepository: Repository<PendaftaranTesis>,
     @InjectRepository(Pengguna)
     private penggunaRepository: Repository<Pengguna>,
     @InjectRepository(Topik)
     private topicRepostitory: Repository<Topik>,
+    @InjectRepository(DosenBimbingan)
+    private dosenBimbinganRepository: Repository<DosenBimbingan>,
+    private dataSource: DataSource,
+    private penggunaService: PenggunaService,
+    private httpService: HttpService,
   ) {}
 
   async createTopicRegistration(
     userId: string,
-    topicRegistrationDto: RegistrasiTopikDto,
-  ): Promise<PengajuanPengambilanTopik> {
-    // TODO: Proper validations
-
-    // Validate id
-    validateId([
-      { id: userId, object: "Pengguna" },
-      { id: topicRegistrationDto.idPembimbing, object: "Pembimbing" },
-    ]);
-
-    // Validate user id, supervisor id
-    const [user, supervisor, topic] = await Promise.all([
-      this.penggunaRepository.findOne({
-        where: { id: userId },
+    topicRegistrationDto: RegDto,
+  ): Promise<IdDto> {
+    const queries: (
+      | Promise<void | PendaftaranTesis>
+      | Promise<Pengguna>
+      | Promise<Topik>
+    )[] = [
+      this.getNewestRegByMhsOrFail(userId).catch((ex: BadRequestException) => {
+        if (ex.message === "No mahasiswa user with given id exists") {
+          throw ex;
+        }
+        // else: mahasiswa does not have pending registration -> allowed
       }),
       this.penggunaRepository.findOne({
-        where: { id: topicRegistrationDto.idPembimbing },
+        where: { id: topicRegistrationDto.idPenerima },
       }),
-      this.topicRepostitory.findOne({
-        where: { judul: topicRegistrationDto.judulTopik },
-      }),
-    ]);
+    ];
+
+    if (topicRegistrationDto.idTopik) {
+      queries.push(
+        this.topicRepostitory.findOne({
+          where: { id: topicRegistrationDto.idTopik },
+        }),
+      );
+    }
+
+    const queryResult = await Promise.all(queries);
+    const lastPendaftaran = queryResult[0] as PendaftaranTesis;
+    const penerima = queryResult[1] as Pengguna;
+    let topik = topicRegistrationDto.idTopik ? (queryResult[2] as Topik) : null;
+
+    if (!penerima) {
+      throw new NotFoundException("Penerima not found.");
+    }
+
+    if (topicRegistrationDto.idTopik) {
+      if (!topik) {
+        throw new NotFoundException("Topic not found.");
+      }
 
-    if (!user) {
-      throw new NotFoundException("User not found.");
-    } else if (!supervisor) {
-      throw new NotFoundException("Supervisor not found.");
-    } else if (!topic) {
-      throw new NotFoundException("Topic not found.");
+      if (!topik.aktif) {
+        throw new BadRequestException("Topic is not active.");
+      }
+    }
+
+    if (lastPendaftaran && lastPendaftaran.status !== RegStatus.REJECTED) {
+      throw new BadRequestException(
+        "Mahasiswa already has pending registration in this period",
+      );
+    }
+
+    if (!topik) {
+      if (
+        !topicRegistrationDto.judulTopik ||
+        !topicRegistrationDto.deskripsiTopik
+      ) {
+        throw new BadRequestException(
+          "Judul dan deskripsi topik tidak boleh kosong.",
+        );
+      }
+
+      topik = this.topicRepostitory.create({
+        judul: topicRegistrationDto.judulTopik,
+        deskripsi: topicRegistrationDto.deskripsiTopik,
+        idPengaju: userId,
+      });
     }
 
     // Create new registration
-    const createdRegistration = this.pengajuanPengambilanTopikRepository.create(
-      {
-        ...topicRegistrationDto,
-        waktuPengiriman: new Date(),
-        mahasiswa: user,
-        pembimbing: supervisor,
-        topik: topic,
+    const createdRegistration = this.pendaftaranTesisRepository.create({
+      ...topicRegistrationDto,
+      mahasiswaId: userId,
+      penerima,
+      topik,
+    });
+
+    await this.pendaftaranTesisRepository.save(createdRegistration);
+
+    return {
+      id: createdRegistration.id,
+    };
+  }
+
+  async findByUserId(
+    mahasiswaId: string,
+    isNewestOnly: boolean,
+    idPenerima?: string,
+  ) {
+    await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId);
+
+    const baseQuery = this.pendaftaranTesisRepository
+      .createQueryBuilder("pt")
+      .select([
+        "pt.id",
+        "pt.jadwalInterview",
+        "pt.status",
+        "pt.jalurPilihan",
+        "pt.waktuPengiriman",
+        "topik.judul",
+        "topik.deskripsi",
+        "penerima.id",
+        "penerima.nama",
+        "penerima.kontakWhatsApp",
+        "penerima.kontakMsTeams",
+        "penerima.kontakEmail",
+        "penerima.kontakTelp",
+        "penerima.kontakLainnya",
+        "dosenBimbingan",
+        "dosen.id",
+        "dosen.nama",
+        "dosen.kontakWhatsApp",
+        "dosen.kontakMsTeams",
+        "dosen.kontakEmail",
+        "dosen.kontakTelp",
+        "dosen.kontakLainnya",
+      ])
+      .leftJoin("pt.topik", "topik")
+      .leftJoin("pt.penerima", "penerima")
+      .leftJoin("pt.dosenBimbingan", "dosenBimbingan")
+      .leftJoin("dosenBimbingan.dosen", "dosen")
+      .where("pt.mahasiswaId = :mahasiswaId", { mahasiswaId })
+      .orderBy("pt.waktuPengiriman", "DESC");
+
+    const res = await baseQuery.getMany();
+
+    if (res.length === 0) {
+      throw new NotFoundException("Tidak ada registrasi tesis yang ditemukan.");
+    }
+
+    if (idPenerima) {
+      // requester only has S2_PEMBIMBING access
+      const reg = res[0];
+
+      if (reg.penerima.id !== idPenerima) {
+        throw new ForbiddenException();
+      }
+    }
+
+    const mappedRes = res.map((r) => ({
+      id: r.id,
+      jadwalInterview: r.jadwalInterview,
+      jalurPilihan: r.jalurPilihan,
+      status: r.status,
+      waktuPengiriman: r.waktuPengiriman,
+      judulTopik: r.topik.judul,
+      deskripsiTopik: r.topik.deskripsi,
+      dosenPembimbing:
+        r.status === RegStatus.APPROVED
+          ? r.dosenBimbingan.map((db) => db.dosen)
+          : [r.penerima],
+    }));
+
+    if (isNewestOnly) {
+      // only get last registration
+      // slow performance because get all records first then only returns the first one
+      // need to change to use subquery
+      mappedRes.splice(1);
+    }
+
+    return mappedRes;
+  }
+
+  async getRegsStatistics(options: {
+    idPenerima?: string;
+  }): Promise<RegStatisticsRespDto> {
+    let totalMahasiswa = this.penggunaRepository.count({
+      where: { roles: ArrayContains([RoleEnum.S2_MAHASISWA]) },
+    });
+
+    // Show newest regs per Mhs
+    const baseQuery = this.pendaftaranTesisRepository
+      .createQueryBuilder("pt")
+      .innerJoinAndSelect(
+        (qb) =>
+          qb
+            .select([
+              "pt.mahasiswaId AS latest_mahasiswaId",
+              "MAX(pt.waktuPengiriman) AS latestPengiriman",
+            ])
+            .from(PendaftaranTesis, "pt")
+            .groupBy("pt.mahasiswaId"),
+        "latest",
+        "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman",
+      )
+      .innerJoin("pt.mahasiswa", "mahasiswa")
+      .where("mahasiswa.aktif = true");
+
+    if (options.idPenerima) {
+      baseQuery.andWhere("pt.penerimaId = :idPenerima", {
+        idPenerima: options.idPenerima,
+      });
+
+      totalMahasiswa = baseQuery.getCount();
+    }
+
+    const totalDiterima = baseQuery
+      .clone()
+      .andWhere("pt.status = :status", { status: RegStatus.APPROVED })
+      .getCount();
+
+    const totalProses = baseQuery
+      .clone()
+      .andWhere("pt.status IN (:...status)", {
+        status: [RegStatus.NOT_ASSIGNED, RegStatus.INTERVIEW],
+      })
+      .getCount();
+
+    const totalDitolak = baseQuery
+      .clone()
+      .andWhere("pt.status = :status", { status: RegStatus.REJECTED })
+      .getCount();
+
+    const [total, diterima, proses, ditolak] = await Promise.all([
+      totalMahasiswa,
+      totalDiterima,
+      totalProses,
+      totalDitolak,
+    ]);
+
+    return {
+      diterima: {
+        amount: diterima,
+        percentage: Math.round((diterima / total) * 100),
+      },
+      sedang_proses: {
+        amount: proses,
+        percentage: Math.round((proses / total) * 100),
+      },
+      ditolak: {
+        amount: ditolak,
+        percentage: Math.round((ditolak / total) * 100),
+      },
+    };
+  }
+
+  async findAllRegs(options: {
+    status?: RegStatus;
+    page: number;
+    limit?: number;
+    idPenerima?: string;
+    search?: string;
+    order_by?: "nim";
+    sort?: "ASC" | "DESC";
+  }) {
+    const baseQuery = this.pendaftaranTesisRepository
+      .createQueryBuilder("pt")
+      .select("pt");
+
+    // Show newest regs per Mhs
+    // May need to make materialized view to improve performance
+    baseQuery.innerJoinAndSelect(
+      (qb) =>
+        qb
+          .select([
+            "pt.mahasiswaId AS latest_mahasiswaId",
+            "MAX(pt.waktuPengiriman) AS latestPengiriman",
+          ])
+          .from(PendaftaranTesis, "pt")
+          .groupBy("pt.mahasiswaId"),
+      "latest",
+      "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman",
+    );
+
+    baseQuery
+      .innerJoinAndSelect("pt.penerima", "penerima")
+      .innerJoinAndSelect("pt.mahasiswa", "mahasiswa")
+      .where("mahasiswa.aktif = true");
+
+    if (options.idPenerima) {
+      baseQuery.andWhere("pt.penerimaId = :idPenerima", {
+        idPenerima: options.idPenerima,
+      });
+    }
+
+    if (options.search) {
+      baseQuery.andWhere(
+        new Brackets((qb) =>
+          qb
+            .where("mahasiswa.nama ILIKE :search", {
+              search: `%${options.search}%`,
+            })
+            .orWhere("mahasiswa.nim ILIKE :search", {
+              search: `%${options.search}%`,
+            }),
+        ),
+      );
+    }
+
+    if (options.status) {
+      baseQuery.andWhere("pt.status = :status", { status: options.status });
+    }
+
+    if (options.order_by) {
+      const orderByMapping = {
+        nim: "CAST(mahasiswa.nim AS INTEGER)",
+      };
+
+      baseQuery.orderBy(
+        generateQueryBuilderOrderByObj(
+          orderByMapping,
+          options.order_by,
+          options.sort,
+        ),
+      );
+    }
+
+    if (options.limit) {
+      baseQuery.take(options.limit);
+      baseQuery.skip((options.page - 1) * options.limit);
+    }
+
+    const [data, count] = await baseQuery.getManyAndCount();
+
+    const resData: FindAllNewestRegRespDto = {
+      data: data.map((reg) => ({
+        pendaftaran_id: reg.id,
+        nim: reg.mahasiswa.nim,
+        mahasiswa_id: reg.mahasiswa.id,
+        mahasiswa_nama: reg.mahasiswa.nama,
+        pembimbing_nama: reg.penerima.nama,
+        status: reg.status,
+        jadwal_interview: reg.jadwalInterview,
+      })),
+      count,
+    };
+
+    return resData;
+  }
+
+  async getNewestRegByMhsOrFail(mahasiswaId: string) {
+    const mahasiswa = await this.penggunaRepository.findOne({
+      select: {
+        id: true,
+        roles: true,
       },
+      where: {
+        id: mahasiswaId,
+        aktif: true,
+      },
+    });
+
+    if (!mahasiswa || !mahasiswa.roles.includes(RoleEnum.S2_MAHASISWA))
+      throw new BadRequestException(
+        "No active mahasiswa user with given id exists",
+      );
+
+    const newestReg = await this.pendaftaranTesisRepository.findOne({
+      select: {
+        id: true,
+        jadwalInterview: true,
+        status: true,
+        waktuPengiriman: true,
+        jalurPilihan: true,
+        topik: {
+          judul: true,
+          deskripsi: true,
+        },
+        penerima: {
+          id: true,
+        },
+      },
+      relations: {
+        topik: true,
+        penerima: true,
+        mahasiswa: true,
+      },
+      where: {
+        mahasiswa: mahasiswa,
+      },
+      order: {
+        waktuPengiriman: "DESC",
+      },
+    });
+
+    if (!newestReg)
+      throw new BadRequestException(
+        "Mahasiswa does not have pending registration in this period",
+      );
+
+    return newestReg;
+  }
+
+  async updateInterviewDate(
+    mahasiswaId: string,
+    dto: UpdateInterviewBodyDto,
+    req: Request,
+    idPenerima?: string,
+  ) {
+    await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId);
+
+    const minDate = new Date();
+    minDate.setDate(minDate.getDate() + 2);
+
+    if (dayjs(dto.date).isBefore(dayjs(minDate).endOf("d"))) {
+      throw new BadRequestException(
+        "Interview date must be at least 2 days from now",
+      );
+    }
+
+    const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId);
+
+    if (newestReg && idPenerima && newestReg.penerima.id !== idPenerima) {
+      throw new ForbiddenException();
+    }
+
+    const restrictedStatus: RegStatus[] = [
+      RegStatus.APPROVED,
+      RegStatus.REJECTED,
+    ];
+
+    if (restrictedStatus.includes(newestReg.status))
+      throw new BadRequestException(
+        newestReg.status == RegStatus.APPROVED
+          ? "Cannot set interview for registration that is already accepted"
+          : "Mahasiswa does not have pending registration",
+      );
+
+    const newDate = new Date(dto.date);
+
+    let token = "";
+    if (req?.cookies?.[process.env.COOKIE_NAME]) {
+      token = req.cookies[process.env.COOKIE_NAME];
+    }
+    if (req.headers?.authorization) {
+      token = req.headers.authorization.slice(7);
+    }
+
+    const { data: notif } = await firstValueFrom(
+      this.httpService.post(
+        `${process.env.AUTH_SERVICE_URL}/notifikasi`,
+        {
+          title: "Jadwal interview pendaftaran Anda telah diubah",
+          description:
+            "Jadwal interview pendaftaran tesis Anda telah diubah. Silahkan periksa kembali data Anda untuk mengetahui lebih lanjut.",
+          penggunaId: mahasiswaId,
+        },
+        {
+          headers: {
+            Authorization: `Bearer ${token}`,
+          },
+        },
+      ),
     );
 
-    await this.pengajuanPengambilanTopikRepository.save(createdRegistration);
+    try {
+      await this.pendaftaranTesisRepository.update(
+        { id: newestReg.id },
+        { jadwalInterview: newDate, status: RegStatus.INTERVIEW },
+      );
+    } catch {
+      await firstValueFrom(
+        this.httpService.delete(
+          `${process.env.AUTH_SERVICE_URL}/notifikasi/${notif.id}`,
+          {
+            headers: {
+              Authorization: `Bearer ${token}`,
+            },
+          },
+        ),
+      );
+    }
 
-    return createdRegistration;
+    return { id: newestReg.id } as IdDto;
   }
 
-  async findByUserId(mahasiswaId: string) {
-    return await this.pengajuanPengambilanTopikRepository.find({
-      relations: ["topik", "pembimbing"],
-      where: { mahasiswa: { id: mahasiswaId } },
+  async updateStatus(
+    mahasiswaId: string,
+    dto: UpdateStatusBodyDto,
+    req: Request,
+    idPenerima?: string,
+  ) {
+    await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId);
+
+    const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId);
+
+    if (newestReg && idPenerima && newestReg.penerima.id !== idPenerima) {
+      throw new ForbiddenException();
+    }
+
+    const queryRunner = this.dataSource.createQueryRunner();
+    await queryRunner.connect();
+    await queryRunner.startTransaction();
+
+    let token = "";
+    if (req?.cookies?.[process.env.COOKIE_NAME]) {
+      token = req.cookies[process.env.COOKIE_NAME];
+    }
+    if (req.headers?.authorization) {
+      token = req.headers.authorization.slice(7);
+    }
+
+    const { data: notif } = await firstValueFrom(
+      this.httpService.post(
+        `${process.env.AUTH_SERVICE_URL}/notifikasi`,
+        {
+          title: `Pendaftaran tesis Anda ${dto.status === RegStatus.APPROVED ? "diterima" : "ditolak"}`,
+          description: `Pendaftaran tesis Anda ${dto.status === RegStatus.APPROVED ? "diterima" : "ditolak"}. Silahkan periksa kembali data Anda untuk mengetahui lebih lanjut.`,
+          penggunaId: mahasiswaId,
+        },
+        {
+          headers: {
+            Authorization: `Bearer ${token}`,
+          },
+        },
+      ),
+    );
+
+    try {
+      await queryRunner.manager.update(
+        PendaftaranTesis,
+        { id: newestReg.id },
+        { status: dto.status, waktuKeputusan: new Date() },
+      );
+
+      if (dto.status === RegStatus.APPROVED) {
+        await queryRunner.manager.insert(DosenBimbingan, {
+          idPendaftaran: newestReg.id,
+          idDosen: newestReg.penerima.id,
+        });
+      } else {
+        // dto.status === RegStatus.REJECTED
+        await queryRunner.manager.delete(DosenBimbingan, {
+          idPendaftaran: newestReg.id,
+        });
+      }
+
+      await queryRunner.commitTransaction();
+    } catch (err) {
+      await queryRunner.rollbackTransaction();
+
+      await firstValueFrom(
+        this.httpService.delete(
+          `${process.env.AUTH_SERVICE_URL}/notifikasi/${notif.id}`,
+          {
+            headers: {
+              Authorization: `Bearer ${token}`,
+            },
+          },
+        ),
+      );
+
+      throw new InternalServerErrorException();
+    } finally {
+      await queryRunner.release();
+    }
+
+    return { id: newestReg.id } as IdDto;
+  }
+
+  async updatePembimbingList(
+    mahasiswaId: string,
+    { pembimbing_ids: dosen_ids }: UpdatePembimbingBodyDto,
+    req: Request,
+  ) {
+    await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId);
+
+    const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId);
+
+    if (newestReg.status !== RegStatus.APPROVED)
+      throw new BadRequestException(
+        "Cannot update pembimbing on non-approved registration",
+      );
+
+    const newPembimbingList = await this.penggunaRepository.findBy({
+      id: In(dosen_ids),
     });
+
+    if (
+      newPembimbingList.length !== dosen_ids.length ||
+      newPembimbingList.some(
+        (dosen) => !dosen.roles.includes(RoleEnum.S2_PEMBIMBING),
+      )
+    )
+      throw new BadRequestException("Dosen id list contains invalid user ids");
+
+    const currentPembimbing = await this.dosenBimbinganRepository.findBy({
+      idPendaftaran: newestReg.id,
+    });
+
+    const newPembimbingIds = newPembimbingList.map((dosen) => dosen.id);
+    const currentPembimbingIds = currentPembimbing.map(
+      (currentPembimbing) => currentPembimbing.idDosen,
+    );
+
+    const idsToBeAdded = newPembimbingIds.filter(
+      (newId) => !currentPembimbingIds.includes(newId),
+    );
+
+    const idsToBeDeleted = currentPembimbingIds.filter(
+      (newId) => !newPembimbingIds.includes(newId),
+    );
+
+    const queryRunner = this.dataSource.createQueryRunner();
+    await queryRunner.connect();
+    await queryRunner.startTransaction();
+
+    let token = "";
+    if (req?.cookies?.[process.env.COOKIE_NAME]) {
+      token = req.cookies[process.env.COOKIE_NAME];
+    }
+    if (req.headers?.authorization) {
+      token = req.headers.authorization.slice(7);
+    }
+
+    const { data: notif } = await firstValueFrom(
+      this.httpService.post(
+        `${process.env.AUTH_SERVICE_URL}/notifikasi`,
+        {
+          title: "Dosen pembimbing Anda telah diubah",
+          description:
+            "Dosen pembimbing Anda telah diubah. Silahkan periksa kembali data Anda untuk mengetahui lebih lanjut.",
+          penggunaId: mahasiswaId,
+        },
+        {
+          headers: {
+            Authorization: `Bearer ${token}`,
+          },
+        },
+      ),
+    );
+
+    try {
+      await queryRunner.manager.insert(
+        DosenBimbingan,
+        idsToBeAdded.map((idDosen) => ({ pendaftaran: newestReg, idDosen })),
+      );
+      await queryRunner.manager.delete(DosenBimbingan, {
+        idDosen: In(idsToBeDeleted),
+      });
+
+      await queryRunner.commitTransaction();
+    } catch (err) {
+      await queryRunner.rollbackTransaction();
+
+      await firstValueFrom(
+        this.httpService.delete(
+          `${process.env.AUTH_SERVICE_URL}/notifikasi/${notif.id}`,
+          {
+            headers: {
+              Authorization: `Bearer ${token}`,
+            },
+          },
+        ),
+      );
+
+      throw new InternalServerErrorException();
+    } finally {
+      await queryRunner.release();
+    }
+
+    return { id: newestReg.id } as IdDto;
   }
 }