init project starter from nest.js
This commit is contained in:
		
							parent
							
								
									db23df0fd1
								
							
						
					
					
						commit
						57872f9f1c
					
				
							
								
								
									
										58
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					# compiled output
 | 
				
			||||||
 | 
					/dist
 | 
				
			||||||
 | 
					/node_modules
 | 
				
			||||||
 | 
					/build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Logs
 | 
				
			||||||
 | 
					logs
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					npm-debug.log*
 | 
				
			||||||
 | 
					pnpm-debug.log*
 | 
				
			||||||
 | 
					yarn-debug.log*
 | 
				
			||||||
 | 
					yarn-error.log*
 | 
				
			||||||
 | 
					lerna-debug.log*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# OS
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Tests
 | 
				
			||||||
 | 
					/coverage
 | 
				
			||||||
 | 
					/.nyc_output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# IDEs and editors
 | 
				
			||||||
 | 
					/.idea
 | 
				
			||||||
 | 
					.project
 | 
				
			||||||
 | 
					.classpath
 | 
				
			||||||
 | 
					.c9/
 | 
				
			||||||
 | 
					*.launch
 | 
				
			||||||
 | 
					.settings/
 | 
				
			||||||
 | 
					*.sublime-workspace
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# IDE - VSCode
 | 
				
			||||||
 | 
					.vscode/*
 | 
				
			||||||
 | 
					!.vscode/settings.json
 | 
				
			||||||
 | 
					!.vscode/tasks.json
 | 
				
			||||||
 | 
					!.vscode/launch.json
 | 
				
			||||||
 | 
					!.vscode/extensions.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# dotenv environment variable files
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					.env.development.local
 | 
				
			||||||
 | 
					.env.test.local
 | 
				
			||||||
 | 
					.env.production.local
 | 
				
			||||||
 | 
					.env.local
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# temp directory
 | 
				
			||||||
 | 
					.temp
 | 
				
			||||||
 | 
					.tmp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Runtime data
 | 
				
			||||||
 | 
					pids
 | 
				
			||||||
 | 
					*.pid
 | 
				
			||||||
 | 
					*.seed
 | 
				
			||||||
 | 
					*.pid.lock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Diagnostic reports (https://nodejs.org/api/report.html)
 | 
				
			||||||
 | 
					report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/generated/prisma
 | 
				
			||||||
							
								
								
									
										4
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "singleQuote": true,
 | 
				
			||||||
 | 
					  "trailingComma": "all"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "postman.settings.dotenv-detection-notification-visibility": false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					// @ts-check
 | 
				
			||||||
 | 
					import eslint from '@eslint/js';
 | 
				
			||||||
 | 
					import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
 | 
				
			||||||
 | 
					import globals from 'globals';
 | 
				
			||||||
 | 
					import tseslint from 'typescript-eslint';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default tseslint.config(
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    ignores: ['eslint.config.mjs'],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  eslint.configs.recommended,
 | 
				
			||||||
 | 
					  ...tseslint.configs.recommendedTypeChecked,
 | 
				
			||||||
 | 
					  eslintPluginPrettierRecommended,
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    languageOptions: {
 | 
				
			||||||
 | 
					      globals: {
 | 
				
			||||||
 | 
					        ...globals.node,
 | 
				
			||||||
 | 
					        ...globals.jest,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      sourceType: 'commonjs',
 | 
				
			||||||
 | 
					      parserOptions: {
 | 
				
			||||||
 | 
					        projectService: true,
 | 
				
			||||||
 | 
					        tsconfigRootDir: import.meta.dirname,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    rules: {
 | 
				
			||||||
 | 
					      '@typescript-eslint/no-explicit-any': 'off',
 | 
				
			||||||
 | 
					      '@typescript-eslint/no-floating-promises': 'warn',
 | 
				
			||||||
 | 
					      '@typescript-eslint/no-unsafe-argument': 'warn',
 | 
				
			||||||
 | 
					      "prettier/prettier": ["error", { endOfLine: "auto" }],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										8
									
								
								nest-cli.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								nest-cli.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "$schema": "https://json.schemastore.org/nest-cli",
 | 
				
			||||||
 | 
					  "collection": "@nestjs/schematics",
 | 
				
			||||||
 | 
					  "sourceRoot": "src",
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "deleteOutDir": true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11196
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										11196
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										86
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "hadirapp-backend",
 | 
				
			||||||
 | 
					  "version": "0.0.1",
 | 
				
			||||||
 | 
					  "description": "",
 | 
				
			||||||
 | 
					  "author": "",
 | 
				
			||||||
 | 
					  "private": true,
 | 
				
			||||||
 | 
					  "license": "UNLICENSED",
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "build": "nest build",
 | 
				
			||||||
 | 
					    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
 | 
				
			||||||
 | 
					    "start": "nest start",
 | 
				
			||||||
 | 
					    "start:dev": "nest start --watch",
 | 
				
			||||||
 | 
					    "start:debug": "nest start --debug --watch",
 | 
				
			||||||
 | 
					    "start:prod": "node dist/main",
 | 
				
			||||||
 | 
					    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
 | 
				
			||||||
 | 
					    "test": "jest",
 | 
				
			||||||
 | 
					    "test:watch": "jest --watch",
 | 
				
			||||||
 | 
					    "test:cov": "jest --coverage",
 | 
				
			||||||
 | 
					    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
 | 
				
			||||||
 | 
					    "test:e2e": "jest --config ./test/jest-e2e.json"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@nestjs/common": "^11.0.1",
 | 
				
			||||||
 | 
					    "@nestjs/config": "^4.0.2",
 | 
				
			||||||
 | 
					    "@nestjs/core": "^11.0.1",
 | 
				
			||||||
 | 
					    "@nestjs/jwt": "^11.0.1",
 | 
				
			||||||
 | 
					    "@nestjs/passport": "^11.0.5",
 | 
				
			||||||
 | 
					    "@nestjs/platform-express": "^11.1.8",
 | 
				
			||||||
 | 
					    "@nestjs/swagger": "^11.2.1",
 | 
				
			||||||
 | 
					    "@prisma/client": "^6.18.0",
 | 
				
			||||||
 | 
					    "bcrypt": "^6.0.0",
 | 
				
			||||||
 | 
					    "class-transformer": "^0.5.1",
 | 
				
			||||||
 | 
					    "class-validator": "^0.14.2",
 | 
				
			||||||
 | 
					    "multer": "^2.0.2",
 | 
				
			||||||
 | 
					    "passport": "^0.7.0",
 | 
				
			||||||
 | 
					    "passport-jwt": "^4.0.1",
 | 
				
			||||||
 | 
					    "reflect-metadata": "^0.2.2",
 | 
				
			||||||
 | 
					    "swagger-ui-express": "^5.0.1",
 | 
				
			||||||
 | 
					    "uuid": "^13.0.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@eslint/eslintrc": "^3.2.0",
 | 
				
			||||||
 | 
					    "@eslint/js": "^9.18.0",
 | 
				
			||||||
 | 
					    "@nestjs/cli": "^11.0.0",
 | 
				
			||||||
 | 
					    "@nestjs/schematics": "^11.0.0",
 | 
				
			||||||
 | 
					    "@nestjs/testing": "^11.0.1",
 | 
				
			||||||
 | 
					    "@types/express": "^5.0.0",
 | 
				
			||||||
 | 
					    "@types/jest": "^30.0.0",
 | 
				
			||||||
 | 
					    "@types/node": "^22.10.7",
 | 
				
			||||||
 | 
					    "@types/supertest": "^6.0.2",
 | 
				
			||||||
 | 
					    "@types/uuid": "^10.0.0",
 | 
				
			||||||
 | 
					    "eslint": "^9.18.0",
 | 
				
			||||||
 | 
					    "eslint-config-prettier": "^10.0.1",
 | 
				
			||||||
 | 
					    "eslint-plugin-prettier": "^5.2.2",
 | 
				
			||||||
 | 
					    "globals": "^16.0.0",
 | 
				
			||||||
 | 
					    "jest": "^30.0.0",
 | 
				
			||||||
 | 
					    "prettier": "^3.4.2",
 | 
				
			||||||
 | 
					    "prisma": "^6.18.0",
 | 
				
			||||||
 | 
					    "rxjs": "^7.8.2",
 | 
				
			||||||
 | 
					    "source-map-support": "^0.5.21",
 | 
				
			||||||
 | 
					    "supertest": "^7.0.0",
 | 
				
			||||||
 | 
					    "ts-jest": "^29.2.5",
 | 
				
			||||||
 | 
					    "ts-loader": "^9.5.2",
 | 
				
			||||||
 | 
					    "ts-node": "^10.9.2",
 | 
				
			||||||
 | 
					    "tsconfig-paths": "^4.2.0",
 | 
				
			||||||
 | 
					    "typescript": "^5.9.3",
 | 
				
			||||||
 | 
					    "typescript-eslint": "^8.20.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "jest": {
 | 
				
			||||||
 | 
					    "moduleFileExtensions": [
 | 
				
			||||||
 | 
					      "js",
 | 
				
			||||||
 | 
					      "json",
 | 
				
			||||||
 | 
					      "ts"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "rootDir": "src",
 | 
				
			||||||
 | 
					    "testRegex": ".*\\.spec\\.ts$",
 | 
				
			||||||
 | 
					    "transform": {
 | 
				
			||||||
 | 
					      "^.+\\.(t|j)s$": "ts-jest"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "collectCoverageFrom": [
 | 
				
			||||||
 | 
					      "**/*.(t|j)s"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "coverageDirectory": "../coverage",
 | 
				
			||||||
 | 
					    "testEnvironment": "node"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								prisma.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								prisma.config.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { defineConfig, env } from "prisma/config";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default defineConfig({
 | 
				
			||||||
 | 
					  schema: "prisma/schema.prisma",
 | 
				
			||||||
 | 
					  migrations: {
 | 
				
			||||||
 | 
					    path: "prisma/migrations",
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  engine: "classic",
 | 
				
			||||||
 | 
					  datasource: {
 | 
				
			||||||
 | 
					    url: env("DATABASE_URL"),
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										256
									
								
								prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,256 @@
 | 
				
			|||||||
 | 
					generator client {
 | 
				
			||||||
 | 
					  provider = "prisma-client-js"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					datasource db {
 | 
				
			||||||
 | 
					  provider = "mysql"
 | 
				
			||||||
 | 
					  url      = env("DATABASE_URL")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model admins {
 | 
				
			||||||
 | 
					  id        String   @id
 | 
				
			||||||
 | 
					  userId    String   @unique
 | 
				
			||||||
 | 
					  name      String
 | 
				
			||||||
 | 
					  phone     String?
 | 
				
			||||||
 | 
					  createdAt DateTime @default(now())
 | 
				
			||||||
 | 
					  updatedAt DateTime
 | 
				
			||||||
 | 
					  users     users    @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model attendance_sessions {
 | 
				
			||||||
 | 
					  id          String        @id
 | 
				
			||||||
 | 
					  scheduleId  String
 | 
				
			||||||
 | 
					  date        DateTime      @db.Date
 | 
				
			||||||
 | 
					  startTime   DateTime
 | 
				
			||||||
 | 
					  endTime     DateTime
 | 
				
			||||||
 | 
					  qrCode      String?       @unique
 | 
				
			||||||
 | 
					  qrExpiredAt DateTime?
 | 
				
			||||||
 | 
					  topic       String?
 | 
				
			||||||
 | 
					  notes       String?       @db.Text
 | 
				
			||||||
 | 
					  isActive    Boolean       @default(true)
 | 
				
			||||||
 | 
					  createdAt   DateTime      @default(now())
 | 
				
			||||||
 | 
					  updatedAt   DateTime
 | 
				
			||||||
 | 
					  schedules   schedules     @relation(fields: [scheduleId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					  attendances attendances[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@unique([scheduleId, date])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model attendances {
 | 
				
			||||||
 | 
					  id                  String              @id
 | 
				
			||||||
 | 
					  sessionId           String
 | 
				
			||||||
 | 
					  studentId           String
 | 
				
			||||||
 | 
					  status              attendances_status  @default(PRESENT)
 | 
				
			||||||
 | 
					  checkInTime         DateTime            @default(now())
 | 
				
			||||||
 | 
					  ipAddress           String?
 | 
				
			||||||
 | 
					  latitude            Float?
 | 
				
			||||||
 | 
					  longitude           Float?
 | 
				
			||||||
 | 
					  deviceInfo          String?             @db.Text
 | 
				
			||||||
 | 
					  notes               String?             @db.Text
 | 
				
			||||||
 | 
					  createdAt           DateTime            @default(now())
 | 
				
			||||||
 | 
					  updatedAt           DateTime
 | 
				
			||||||
 | 
					  attendance_sessions attendance_sessions @relation(fields: [sessionId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					  students            students            @relation(fields: [studentId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@unique([sessionId, studentId])
 | 
				
			||||||
 | 
					  @@index([studentId], map: "attendances_studentId_fkey")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model audit_logs {
 | 
				
			||||||
 | 
					  id        String   @id
 | 
				
			||||||
 | 
					  userId    String
 | 
				
			||||||
 | 
					  action    String
 | 
				
			||||||
 | 
					  entity    String
 | 
				
			||||||
 | 
					  entityId  String
 | 
				
			||||||
 | 
					  oldValue  String?  @db.Text
 | 
				
			||||||
 | 
					  newValue  String?  @db.Text
 | 
				
			||||||
 | 
					  ipAddress String?
 | 
				
			||||||
 | 
					  userAgent String?  @db.Text
 | 
				
			||||||
 | 
					  createdAt DateTime @default(now())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model classes {
 | 
				
			||||||
 | 
					  id        String      @id
 | 
				
			||||||
 | 
					  name      String      @unique
 | 
				
			||||||
 | 
					  grade     String
 | 
				
			||||||
 | 
					  major     String?
 | 
				
			||||||
 | 
					  capacity  Int         @default(40)
 | 
				
			||||||
 | 
					  createdAt DateTime    @default(now())
 | 
				
			||||||
 | 
					  updatedAt DateTime
 | 
				
			||||||
 | 
					  schedules schedules[]
 | 
				
			||||||
 | 
					  students  students[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model courses {
 | 
				
			||||||
 | 
					  id          String      @id
 | 
				
			||||||
 | 
					  code        String      @unique
 | 
				
			||||||
 | 
					  name        String
 | 
				
			||||||
 | 
					  description String?     @db.Text
 | 
				
			||||||
 | 
					  teacherId   String
 | 
				
			||||||
 | 
					  credits     Int         @default(2)
 | 
				
			||||||
 | 
					  createdAt   DateTime    @default(now())
 | 
				
			||||||
 | 
					  updatedAt   DateTime
 | 
				
			||||||
 | 
					  teachers    teachers    @relation(fields: [teacherId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					  schedules   schedules[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([teacherId], map: "courses_teacherId_fkey")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model leave_requests {
 | 
				
			||||||
 | 
					  id          String                @id
 | 
				
			||||||
 | 
					  studentId   String
 | 
				
			||||||
 | 
					  startDate   DateTime              @db.Date
 | 
				
			||||||
 | 
					  endDate     DateTime              @db.Date
 | 
				
			||||||
 | 
					  type        leave_requests_type
 | 
				
			||||||
 | 
					  reason      String                @db.Text
 | 
				
			||||||
 | 
					  attachment  String?
 | 
				
			||||||
 | 
					  status      leave_requests_status @default(PENDING)
 | 
				
			||||||
 | 
					  reviewedBy  String?
 | 
				
			||||||
 | 
					  reviewedAt  DateTime?
 | 
				
			||||||
 | 
					  reviewNotes String?               @db.Text
 | 
				
			||||||
 | 
					  createdAt   DateTime              @default(now())
 | 
				
			||||||
 | 
					  updatedAt   DateTime
 | 
				
			||||||
 | 
					  students    students              @relation(fields: [studentId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([studentId], map: "leave_requests_studentId_fkey")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model notifications {
 | 
				
			||||||
 | 
					  id        String             @id
 | 
				
			||||||
 | 
					  userId    String
 | 
				
			||||||
 | 
					  type      notifications_type
 | 
				
			||||||
 | 
					  title     String
 | 
				
			||||||
 | 
					  message   String             @db.Text
 | 
				
			||||||
 | 
					  isRead    Boolean            @default(false)
 | 
				
			||||||
 | 
					  createdAt DateTime           @default(now())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model schedules {
 | 
				
			||||||
 | 
					  id                  String                @id
 | 
				
			||||||
 | 
					  courseId            String
 | 
				
			||||||
 | 
					  classId             String
 | 
				
			||||||
 | 
					  teacherId           String
 | 
				
			||||||
 | 
					  dayOfWeek           schedules_dayOfWeek
 | 
				
			||||||
 | 
					  startTime           String
 | 
				
			||||||
 | 
					  endTime             String
 | 
				
			||||||
 | 
					  room                String?
 | 
				
			||||||
 | 
					  wifiNetworkId       String?
 | 
				
			||||||
 | 
					  isActive            Boolean               @default(true)
 | 
				
			||||||
 | 
					  createdAt           DateTime              @default(now())
 | 
				
			||||||
 | 
					  updatedAt           DateTime
 | 
				
			||||||
 | 
					  attendance_sessions attendance_sessions[]
 | 
				
			||||||
 | 
					  classes             classes               @relation(fields: [classId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					  courses             courses               @relation(fields: [courseId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					  teachers            teachers              @relation(fields: [teacherId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					  wifi_networks       wifi_networks?        @relation(fields: [wifiNetworkId], references: [id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([classId], map: "schedules_classId_fkey")
 | 
				
			||||||
 | 
					  @@index([courseId], map: "schedules_courseId_fkey")
 | 
				
			||||||
 | 
					  @@index([teacherId], map: "schedules_teacherId_fkey")
 | 
				
			||||||
 | 
					  @@index([wifiNetworkId], map: "schedules_wifiNetworkId_fkey")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model students {
 | 
				
			||||||
 | 
					  id             String           @id
 | 
				
			||||||
 | 
					  userId         String           @unique
 | 
				
			||||||
 | 
					  nis            String           @unique
 | 
				
			||||||
 | 
					  name           String
 | 
				
			||||||
 | 
					  phone          String?
 | 
				
			||||||
 | 
					  address        String?          @db.Text
 | 
				
			||||||
 | 
					  photo          String?
 | 
				
			||||||
 | 
					  classId        String?
 | 
				
			||||||
 | 
					  parentPhone    String?
 | 
				
			||||||
 | 
					  parentEmail    String?
 | 
				
			||||||
 | 
					  createdAt      DateTime         @default(now())
 | 
				
			||||||
 | 
					  updatedAt      DateTime
 | 
				
			||||||
 | 
					  attendances    attendances[]
 | 
				
			||||||
 | 
					  leave_requests leave_requests[]
 | 
				
			||||||
 | 
					  classes        classes?         @relation(fields: [classId], references: [id])
 | 
				
			||||||
 | 
					  users          users            @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @@index([classId], map: "students_classId_fkey")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model teachers {
 | 
				
			||||||
 | 
					  id        String      @id
 | 
				
			||||||
 | 
					  userId    String      @unique
 | 
				
			||||||
 | 
					  nip       String      @unique
 | 
				
			||||||
 | 
					  name      String
 | 
				
			||||||
 | 
					  phone     String?
 | 
				
			||||||
 | 
					  createdAt DateTime    @default(now())
 | 
				
			||||||
 | 
					  updatedAt DateTime
 | 
				
			||||||
 | 
					  courses   courses[]
 | 
				
			||||||
 | 
					  schedules schedules[]
 | 
				
			||||||
 | 
					  users     users       @relation(fields: [userId], references: [id], onDelete: Cascade)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model users {
 | 
				
			||||||
 | 
					  id        String     @id
 | 
				
			||||||
 | 
					  email     String     @unique
 | 
				
			||||||
 | 
					  password  String
 | 
				
			||||||
 | 
					  role      users_role @default(STUDENT)
 | 
				
			||||||
 | 
					  isActive  Boolean    @default(true)
 | 
				
			||||||
 | 
					  createdAt DateTime   @default(now())
 | 
				
			||||||
 | 
					  updatedAt DateTime
 | 
				
			||||||
 | 
					  admins    admins?
 | 
				
			||||||
 | 
					  students  students?
 | 
				
			||||||
 | 
					  teachers  teachers?
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					model wifi_networks {
 | 
				
			||||||
 | 
					  id          String      @id
 | 
				
			||||||
 | 
					  ssid        String
 | 
				
			||||||
 | 
					  description String?
 | 
				
			||||||
 | 
					  ipRange     String
 | 
				
			||||||
 | 
					  latitude    Float?
 | 
				
			||||||
 | 
					  longitude   Float?
 | 
				
			||||||
 | 
					  radius      Int         @default(50)
 | 
				
			||||||
 | 
					  isActive    Boolean     @default(true)
 | 
				
			||||||
 | 
					  createdAt   DateTime    @default(now())
 | 
				
			||||||
 | 
					  updatedAt   DateTime
 | 
				
			||||||
 | 
					  schedules   schedules[]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum notifications_type {
 | 
				
			||||||
 | 
					  ATTENDANCE_REMINDER
 | 
				
			||||||
 | 
					  ABSENCE_ALERT
 | 
				
			||||||
 | 
					  LEAVE_APPROVED
 | 
				
			||||||
 | 
					  LEAVE_REJECTED
 | 
				
			||||||
 | 
					  SYSTEM
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum attendances_status {
 | 
				
			||||||
 | 
					  PRESENT
 | 
				
			||||||
 | 
					  LATE
 | 
				
			||||||
 | 
					  EXCUSED
 | 
				
			||||||
 | 
					  SICK
 | 
				
			||||||
 | 
					  ABSENT
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum users_role {
 | 
				
			||||||
 | 
					  ADMIN
 | 
				
			||||||
 | 
					  STUDENT
 | 
				
			||||||
 | 
					  TEACHER
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum leave_requests_type {
 | 
				
			||||||
 | 
					  SICK
 | 
				
			||||||
 | 
					  EXCUSED
 | 
				
			||||||
 | 
					  OTHER
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum schedules_dayOfWeek {
 | 
				
			||||||
 | 
					  MONDAY
 | 
				
			||||||
 | 
					  TUESDAY
 | 
				
			||||||
 | 
					  WEDNESDAY
 | 
				
			||||||
 | 
					  THURSDAY
 | 
				
			||||||
 | 
					  FRIDAY
 | 
				
			||||||
 | 
					  SATURDAY
 | 
				
			||||||
 | 
					  SUNDAY
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum leave_requests_status {
 | 
				
			||||||
 | 
					  PENDING
 | 
				
			||||||
 | 
					  APPROVED
 | 
				
			||||||
 | 
					  REJECTED
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/app.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/app.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { AppController } from './app.controller';
 | 
				
			||||||
 | 
					import { AppService } from './app.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AppController', () => {
 | 
				
			||||||
 | 
					  let appController: AppController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const app: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      controllers: [AppController],
 | 
				
			||||||
 | 
					      providers: [AppService],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    appController = app.get<AppController>(AppController);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('root', () => {
 | 
				
			||||||
 | 
					    it('should return "Hello World!"', () => {
 | 
				
			||||||
 | 
					      expect(appController.getRoot()).toBe('{ message: \'HadirApp Backend is running 🚀\' }');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/app.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/app.controller.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					import { Controller, Get } from '@nestjs/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller()
 | 
				
			||||||
 | 
					export class AppController {
 | 
				
			||||||
 | 
					  @Get()
 | 
				
			||||||
 | 
					  getRoot() {
 | 
				
			||||||
 | 
					    return { message: 'HadirApp Backend is running 🚀' };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								src/app.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/app.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { AppController } from './app.controller';
 | 
				
			||||||
 | 
					import { AuthModule } from './modules/auth/auth.module';
 | 
				
			||||||
 | 
					import { UsersModule } from './modules/users/users.module';
 | 
				
			||||||
 | 
					import { PrismaModule } from './modules/prisma/prisma.module';
 | 
				
			||||||
 | 
					import { ConfigModule } from '@nestjs/config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    ConfigModule.forRoot({ isGlobal: true }),
 | 
				
			||||||
 | 
					    PrismaModule,
 | 
				
			||||||
 | 
					    AuthModule,
 | 
				
			||||||
 | 
					    UsersModule,
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  controllers: [AppController],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AppModule {}
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/app.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/app.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AppService {
 | 
				
			||||||
 | 
					  getHello(): string {
 | 
				
			||||||
 | 
					    return 'Hello World!';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/common/decorators/roles.decorator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/common/decorators/roles.decorator.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					import { SetMetadata } from '@nestjs/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ROLES_KEY = 'roles';
 | 
				
			||||||
 | 
					export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
 | 
				
			||||||
							
								
								
									
										5
									
								
								src/common/guards/jwt-auth.guard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/common/guards/jwt-auth.guard.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { AuthGuard } from '@nestjs/passport';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class JwtAuthGuard extends AuthGuard('jwt') {}
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/common/guards/roles.guard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/common/guards/roles.guard.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Reflector } from '@nestjs/core';
 | 
				
			||||||
 | 
					import { ROLES_KEY } from '../decorators/roles.decorator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class RolesGuard implements CanActivate {
 | 
				
			||||||
 | 
					  constructor(private reflector: Reflector) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  canActivate(context: ExecutionContext): boolean {
 | 
				
			||||||
 | 
					    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
 | 
				
			||||||
 | 
					      context.getHandler(),
 | 
				
			||||||
 | 
					      context.getClass(),
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!requiredRoles) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { user } = context.switchToHttp().getRequest();
 | 
				
			||||||
 | 
					    return requiredRoles.includes(user.role);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { NestFactory } from '@nestjs/core';
 | 
				
			||||||
 | 
					import { AppModule } from './app.module';
 | 
				
			||||||
 | 
					import { ValidationPipe } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function bootstrap() {
 | 
				
			||||||
 | 
					  const app = await NestFactory.create(AppModule);
 | 
				
			||||||
 | 
					  app.setGlobalPrefix('api');
 | 
				
			||||||
 | 
					  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const config = new DocumentBuilder()
 | 
				
			||||||
 | 
					    .setTitle('HadirApp API')
 | 
				
			||||||
 | 
					    .setDescription('API documentation for attendance system')
 | 
				
			||||||
 | 
					    .setVersion('1.0')
 | 
				
			||||||
 | 
					    .addBearerAuth()
 | 
				
			||||||
 | 
					    .build();
 | 
				
			||||||
 | 
					  const document = SwaggerModule.createDocument(app, config);
 | 
				
			||||||
 | 
					  SwaggerModule.setup('docs', app, document);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await app.listen(process.env.PORT || 3000);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					bootstrap();
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/modules/auth/auth.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/modules/auth/auth.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { AuthController } from './auth.controller';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AuthController', () => {
 | 
				
			||||||
 | 
					  let controller: AuthController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const module: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      controllers: [AuthController],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    controller = module.get<AuthController>(AuthController);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be defined', () => {
 | 
				
			||||||
 | 
					    expect(controller).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										44
									
								
								src/modules/auth/auth.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/modules/auth/auth.controller.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					import { Body, Controller, Post } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ApiTags, ApiOperation, ApiBody, ApiResponse, ApiProperty } from '@nestjs/swagger';
 | 
				
			||||||
 | 
					import { AuthService } from './auth.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RegisterDto {
 | 
				
			||||||
 | 
					  @ApiProperty({ example: 'user@example.com', description: 'User email' })
 | 
				
			||||||
 | 
					  email: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ApiProperty({ example: 'strongPassword123', description: 'Plain text password' })
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ApiProperty({ example: 'STUDENT', description: 'Role: ADMIN | STUDENT | TEACHER', required: false })
 | 
				
			||||||
 | 
					  role?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LoginDto {
 | 
				
			||||||
 | 
					  @ApiProperty({ example: 'user@example.com' })
 | 
				
			||||||
 | 
					  email: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ApiProperty({ example: 'strongPassword123' })
 | 
				
			||||||
 | 
					  password: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@ApiTags('auth')
 | 
				
			||||||
 | 
					@Controller('auth')
 | 
				
			||||||
 | 
					export class AuthController {
 | 
				
			||||||
 | 
					  constructor(private authService: AuthService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('register')
 | 
				
			||||||
 | 
					  @ApiOperation({ summary: 'Register a new user' })
 | 
				
			||||||
 | 
					  @ApiBody({ type: RegisterDto })
 | 
				
			||||||
 | 
					  @ApiResponse({ status: 201, description: 'User registered successfully' })
 | 
				
			||||||
 | 
					  async register(@Body() body: RegisterDto) {
 | 
				
			||||||
 | 
					    return this.authService.register(body.email, body.password, body.role || 'STUDENT');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Post('login')
 | 
				
			||||||
 | 
					  @ApiOperation({ summary: 'Authenticate user and return JWT' })
 | 
				
			||||||
 | 
					  @ApiBody({ type: LoginDto })
 | 
				
			||||||
 | 
					  @ApiResponse({ status: 200, description: 'Login successful, returns access_token' })
 | 
				
			||||||
 | 
					  async login(@Body() body: LoginDto) {
 | 
				
			||||||
 | 
					    return this.authService.login(body.email, body.password);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/modules/auth/auth.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/modules/auth/auth.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { JwtModule } from '@nestjs/jwt';
 | 
				
			||||||
 | 
					import { PassportModule } from '@nestjs/passport';
 | 
				
			||||||
 | 
					import { AuthService } from './auth.service';
 | 
				
			||||||
 | 
					import { AuthController } from './auth.controller';
 | 
				
			||||||
 | 
					import { UsersModule } from '../users/users.module';
 | 
				
			||||||
 | 
					import { PrismaModule } from '../prisma/prisma.module';
 | 
				
			||||||
 | 
					import { JwtStrategy } from './strategies/jwt.strategy';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [
 | 
				
			||||||
 | 
					    UsersModule,
 | 
				
			||||||
 | 
					    PrismaModule,
 | 
				
			||||||
 | 
					    PassportModule,
 | 
				
			||||||
 | 
					    JwtModule.register({
 | 
				
			||||||
 | 
					      secret: process.env.JWT_SECRET || 'supersecretkey',
 | 
				
			||||||
 | 
					      signOptions: { expiresIn: '1d' },
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  controllers: [AuthController],
 | 
				
			||||||
 | 
					  providers: [AuthService, JwtStrategy],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AuthModule {}
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/modules/auth/auth.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/modules/auth/auth.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { AuthService } from './auth.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AuthService', () => {
 | 
				
			||||||
 | 
					  let service: AuthService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const module: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      providers: [AuthService],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    service = module.get<AuthService>(AuthService);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be defined', () => {
 | 
				
			||||||
 | 
					    expect(service).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										55
									
								
								src/modules/auth/auth.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/modules/auth/auth.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { PrismaService } from '../prisma/prisma.service';
 | 
				
			||||||
 | 
					import { Prisma, users_role } from '@prisma/client';
 | 
				
			||||||
 | 
					import { JwtService } from '@nestjs/jwt';
 | 
				
			||||||
 | 
					import * as bcrypt from 'bcrypt';
 | 
				
			||||||
 | 
					import { v4 as uuidv4 } from 'uuid';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class AuthService {
 | 
				
			||||||
 | 
					  constructor(private prisma: PrismaService, private jwt: JwtService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async register(email: string, password: string, role?: string) {
 | 
				
			||||||
 | 
					    // validate inputs to avoid passing undefined to bcrypt/prisma
 | 
				
			||||||
 | 
					    if (!email || !password) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Email and password are required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (typeof password !== 'string' || password.length < 6) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Password must be a string with at least 6 characters');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // pastikan role sesuai enum di database (uppercase)
 | 
				
			||||||
 | 
					    const allowedRoles = ['ADMIN', 'STUDENT', 'TEACHER'];
 | 
				
			||||||
 | 
					    const normalizedRole = (role || 'STUDENT').toUpperCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!allowedRoles.includes(normalizedRole)) {
 | 
				
			||||||
 | 
					      throw new BadRequestException(`Invalid role. Allowed: ${allowedRoles.join(', ')}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const hashed = await bcrypt.hash(password, 10);
 | 
				
			||||||
 | 
					    const user = await this.prisma.users.create({
 | 
				
			||||||
 | 
					      data: { id: uuidv4(), email, password: hashed, role: normalizedRole as users_role, updatedAt: new Date() },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return { message: 'User registered', user };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async login(email: string, password: string) {
 | 
				
			||||||
 | 
					    // validate inputs to avoid calling Prisma with undefined values
 | 
				
			||||||
 | 
					    if (!email || !password) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Email and password are required');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const user = await this.prisma.users.findUnique({ where: { email } });
 | 
				
			||||||
 | 
					    if (!user) throw new UnauthorizedException('User not found');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const valid = await bcrypt.compare(password, user.password);
 | 
				
			||||||
 | 
					    if (!valid) throw new UnauthorizedException('Invalid credentials');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const token = await this.jwt.signAsync({
 | 
				
			||||||
 | 
					      sub: user.id,
 | 
				
			||||||
 | 
					      email: user.email,
 | 
				
			||||||
 | 
					      role: user.role,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { message: 'Login success', access_token: token };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/modules/auth/strategies/jwt.strategy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/modules/auth/strategies/jwt.strategy.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { PassportStrategy } from '@nestjs/passport';
 | 
				
			||||||
 | 
					import { ExtractJwt, Strategy } from 'passport-jwt';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class JwtStrategy extends PassportStrategy(Strategy) {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    super({
 | 
				
			||||||
 | 
					      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
 | 
				
			||||||
 | 
					      ignoreExpiration: false,
 | 
				
			||||||
 | 
					      secretOrKey: process.env.JWT_SECRET || 'supersecretkey',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async validate(payload: any) {
 | 
				
			||||||
 | 
					    return { userId: payload.sub, email: payload.email, role: payload.role };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/modules/prisma/prisma.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/modules/prisma/prisma.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					import { Global, Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { PrismaService } from './prisma.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Global()
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  providers: [PrismaService],
 | 
				
			||||||
 | 
					  exports: [PrismaService],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class PrismaModule {}
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/modules/prisma/prisma.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/modules/prisma/prisma.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { PrismaClient } from '@prisma/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
 | 
				
			||||||
 | 
					  async onModuleInit() {
 | 
				
			||||||
 | 
					    await this.$connect();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async onModuleDestroy() {
 | 
				
			||||||
 | 
					    await this.$disconnect();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/modules/users/users.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/modules/users/users.controller.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { UsersController } from './users.controller';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('UsersController', () => {
 | 
				
			||||||
 | 
					  let controller: UsersController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const module: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      controllers: [UsersController],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    controller = module.get<UsersController>(UsersController);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be defined', () => {
 | 
				
			||||||
 | 
					    expect(controller).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/modules/users/users.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/modules/users/users.controller.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import { Controller, Get, Param, UseGuards } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { UsersService } from './users.service';
 | 
				
			||||||
 | 
					import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
 | 
				
			||||||
 | 
					import { RolesGuard } from '../../common/guards/roles.guard';
 | 
				
			||||||
 | 
					import { Roles } from '../../common/decorators/roles.decorator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller('users')
 | 
				
			||||||
 | 
					@UseGuards(JwtAuthGuard, RolesGuard)
 | 
				
			||||||
 | 
					export class UsersController {
 | 
				
			||||||
 | 
					  constructor(private usersService: UsersService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get()
 | 
				
			||||||
 | 
					  @Roles('ADMIN')
 | 
				
			||||||
 | 
					  async getAll() {
 | 
				
			||||||
 | 
					    return this.usersService.findAll();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Get(':id')
 | 
				
			||||||
 | 
					  @Roles('ADMIN', 'TEACHER')
 | 
				
			||||||
 | 
					  async getOne(@Param('id') id: string) {
 | 
				
			||||||
 | 
					    return this.usersService.findOne(id);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/modules/users/users.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/modules/users/users.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { Module } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { UsersService } from './users.service';
 | 
				
			||||||
 | 
					import { UsersController } from './users.controller';
 | 
				
			||||||
 | 
					import { PrismaModule } from '../prisma/prisma.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Module({
 | 
				
			||||||
 | 
					  imports: [PrismaModule],
 | 
				
			||||||
 | 
					  providers: [UsersService],
 | 
				
			||||||
 | 
					  controllers: [UsersController],
 | 
				
			||||||
 | 
					  exports: [UsersService],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class UsersModule {}
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/modules/users/users.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/modules/users/users.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { UsersService } from './users.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('UsersService', () => {
 | 
				
			||||||
 | 
					  let service: UsersService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const module: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      providers: [UsersService],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    service = module.get<UsersService>(UsersService);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should be defined', () => {
 | 
				
			||||||
 | 
					    expect(service).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										15
									
								
								src/modules/users/users.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/modules/users/users.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { PrismaService } from '../prisma/prisma.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class UsersService {
 | 
				
			||||||
 | 
					  constructor(private prisma: PrismaService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async findAll() {
 | 
				
			||||||
 | 
					    return this.prisma.users.findMany();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async findOne(id: string) {
 | 
				
			||||||
 | 
					    return this.prisma.users.findUnique({ where: { id } });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								test/app.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								test/app.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { Test, TestingModule } from '@nestjs/testing';
 | 
				
			||||||
 | 
					import { INestApplication } from '@nestjs/common';
 | 
				
			||||||
 | 
					import request from 'supertest';
 | 
				
			||||||
 | 
					import { App } from 'supertest/types';
 | 
				
			||||||
 | 
					import { AppModule } from './../src/app.module';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('AppController (e2e)', () => {
 | 
				
			||||||
 | 
					  let app: INestApplication<App>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    const moduleFixture: TestingModule = await Test.createTestingModule({
 | 
				
			||||||
 | 
					      imports: [AppModule],
 | 
				
			||||||
 | 
					    }).compile();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    app = moduleFixture.createNestApplication();
 | 
				
			||||||
 | 
					    await app.init();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('/ (GET)', () => {
 | 
				
			||||||
 | 
					    return request(app.getHttpServer())
 | 
				
			||||||
 | 
					      .get('/')
 | 
				
			||||||
 | 
					      .expect(200)
 | 
				
			||||||
 | 
					      .expect('Hello World!');
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										9
									
								
								test/jest-e2e.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								test/jest-e2e.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "moduleFileExtensions": ["js", "json", "ts"],
 | 
				
			||||||
 | 
					  "rootDir": ".",
 | 
				
			||||||
 | 
					  "testEnvironment": "node",
 | 
				
			||||||
 | 
					  "testRegex": ".e2e-spec.ts$",
 | 
				
			||||||
 | 
					  "transform": {
 | 
				
			||||||
 | 
					    "^.+\\.(t|j)s$": "ts-jest"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								tsconfig.build.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tsconfig.build.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "extends": "./tsconfig.json",
 | 
				
			||||||
 | 
					  "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "compilerOptions": {
 | 
				
			||||||
 | 
					    "module": "nodenext",
 | 
				
			||||||
 | 
					    "moduleResolution": "nodenext",
 | 
				
			||||||
 | 
					    "resolvePackageJsonExports": true,
 | 
				
			||||||
 | 
					    "esModuleInterop": true,
 | 
				
			||||||
 | 
					    "isolatedModules": true,
 | 
				
			||||||
 | 
					    "declaration": true,
 | 
				
			||||||
 | 
					    "removeComments": true,
 | 
				
			||||||
 | 
					    "emitDecoratorMetadata": true,
 | 
				
			||||||
 | 
					    "experimentalDecorators": true,
 | 
				
			||||||
 | 
					    "allowSyntheticDefaultImports": true,
 | 
				
			||||||
 | 
					    "target": "ES2023",
 | 
				
			||||||
 | 
					    "sourceMap": true,
 | 
				
			||||||
 | 
					    "outDir": "./dist",
 | 
				
			||||||
 | 
					    "baseUrl": "./src",
 | 
				
			||||||
 | 
					    "incremental": true,
 | 
				
			||||||
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
 | 
					    "strictNullChecks": true,
 | 
				
			||||||
 | 
					    "forceConsistentCasingInFileNames": true,
 | 
				
			||||||
 | 
					    "noImplicitAny": false,
 | 
				
			||||||
 | 
					    "strictBindCallApply": false,
 | 
				
			||||||
 | 
					    "noFallthroughCasesInSwitch": false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user