Spaces:
Sleeping
Sleeping
trae-bot commited on
Commit ·
73746a8
0
Parent(s):
Prepare Hugging Face Space deployment
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +10 -0
- .gitignore +9 -0
- Dockerfile +21 -0
- backend/.env +7 -0
- backend/.eslintrc.js +25 -0
- backend/.prettierrc +4 -0
- backend/README.md +99 -0
- backend/nest-cli.json +8 -0
- backend/package-lock.json +0 -0
- backend/package.json +85 -0
- backend/src/admin/admin.controller.spec.ts +18 -0
- backend/src/admin/admin.controller.ts +32 -0
- backend/src/admin/admin.module.ts +13 -0
- backend/src/admin/admin.service.spec.ts +18 -0
- backend/src/admin/admin.service.ts +33 -0
- backend/src/admin/dto/create-course.dto.ts +27 -0
- backend/src/app.controller.spec.ts +22 -0
- backend/src/app.controller.ts +34 -0
- backend/src/app.module.ts +37 -0
- backend/src/app.service.ts +8 -0
- backend/src/auth/auth.controller.spec.ts +18 -0
- backend/src/auth/auth.controller.ts +29 -0
- backend/src/auth/auth.module.ts +28 -0
- backend/src/auth/auth.service.spec.ts +18 -0
- backend/src/auth/auth.service.ts +103 -0
- backend/src/auth/decorators/roles.decorator.ts +4 -0
- backend/src/auth/dto/login.dto.ts +11 -0
- backend/src/auth/dto/register.dto.ts +17 -0
- backend/src/auth/guards/jwt-auth.guard.ts +5 -0
- backend/src/auth/guards/roles.guard.ts +22 -0
- backend/src/auth/strategies/jwt.strategy.ts +31 -0
- backend/src/courses/courses.controller.spec.ts +18 -0
- backend/src/courses/courses.controller.ts +34 -0
- backend/src/courses/courses.module.ts +14 -0
- backend/src/courses/courses.service.spec.ts +18 -0
- backend/src/courses/courses.service.ts +71 -0
- backend/src/entities/course.entity.ts +49 -0
- backend/src/entities/order.entity.ts +57 -0
- backend/src/entities/payment.entity.ts +54 -0
- backend/src/entities/user-course.entity.ts +41 -0
- backend/src/entities/user.entity.ts +51 -0
- backend/src/main.ts +20 -0
- backend/src/orders/dto/create-order.dto.ts +11 -0
- backend/src/orders/orders.controller.spec.ts +18 -0
- backend/src/orders/orders.controller.ts +28 -0
- backend/src/orders/orders.module.ts +14 -0
- backend/src/orders/orders.service.spec.ts +18 -0
- backend/src/orders/orders.service.ts +67 -0
- backend/src/payment/dto/prepare-payment.dto.ts +12 -0
- backend/src/payment/payment.controller.spec.ts +18 -0
.dockerignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
frontend/node_modules
|
| 2 |
+
frontend/.next
|
| 3 |
+
backend/node_modules
|
| 4 |
+
backend/dist
|
| 5 |
+
backend/course_subscription.sqlite
|
| 6 |
+
backend/uploads
|
| 7 |
+
.git
|
| 8 |
+
.trae
|
| 9 |
+
npm-debug.log*
|
| 10 |
+
yarn-error.log*
|
.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
frontend/node_modules
|
| 2 |
+
frontend/.next
|
| 3 |
+
backend/node_modules
|
| 4 |
+
backend/dist
|
| 5 |
+
backend/course_subscription.sqlite
|
| 6 |
+
backend/uploads
|
| 7 |
+
.trae
|
| 8 |
+
npm-debug.log*
|
| 9 |
+
yarn-error.log*
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-bullseye-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY frontend/package*.json ./frontend/
|
| 6 |
+
COPY backend/package*.json ./backend/
|
| 7 |
+
|
| 8 |
+
RUN npm --prefix frontend ci && npm --prefix backend ci
|
| 9 |
+
|
| 10 |
+
COPY frontend ./frontend
|
| 11 |
+
COPY backend ./backend
|
| 12 |
+
|
| 13 |
+
RUN npm --prefix frontend run build && npm --prefix backend run build
|
| 14 |
+
|
| 15 |
+
ENV NODE_ENV=production
|
| 16 |
+
ENV SPACE_PORT=7860
|
| 17 |
+
ENV BACKEND_INTERNAL_URL=http://127.0.0.1:3001
|
| 18 |
+
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
CMD ["sh", "-c", "PORT=3001 npm --prefix backend run start:prod & npm --prefix frontend run start -- -H 0.0.0.0 -p ${SPACE_PORT}"]
|
backend/.env
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DB_HOST=localhost
|
| 2 |
+
DB_PORT=3306
|
| 3 |
+
DB_USERNAME=root
|
| 4 |
+
DB_PASSWORD=root
|
| 5 |
+
DB_DATABASE=course_subscription
|
| 6 |
+
JWT_SECRET=super_secret_jwt_key_for_course_subscription
|
| 7 |
+
PORT=3001
|
backend/.eslintrc.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
parser: '@typescript-eslint/parser',
|
| 3 |
+
parserOptions: {
|
| 4 |
+
project: 'tsconfig.json',
|
| 5 |
+
tsconfigRootDir: __dirname,
|
| 6 |
+
sourceType: 'module',
|
| 7 |
+
},
|
| 8 |
+
plugins: ['@typescript-eslint/eslint-plugin'],
|
| 9 |
+
extends: [
|
| 10 |
+
'plugin:@typescript-eslint/recommended',
|
| 11 |
+
'plugin:prettier/recommended',
|
| 12 |
+
],
|
| 13 |
+
root: true,
|
| 14 |
+
env: {
|
| 15 |
+
node: true,
|
| 16 |
+
jest: true,
|
| 17 |
+
},
|
| 18 |
+
ignorePatterns: ['.eslintrc.js'],
|
| 19 |
+
rules: {
|
| 20 |
+
'@typescript-eslint/interface-name-prefix': 'off',
|
| 21 |
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
| 22 |
+
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
| 23 |
+
'@typescript-eslint/no-explicit-any': 'off',
|
| 24 |
+
},
|
| 25 |
+
};
|
backend/.prettierrc
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"singleQuote": true,
|
| 3 |
+
"trailingComma": "all"
|
| 4 |
+
}
|
backend/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="center">
|
| 2 |
+
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
| 3 |
+
</p>
|
| 4 |
+
|
| 5 |
+
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
| 6 |
+
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
| 7 |
+
|
| 8 |
+
<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>
|
| 9 |
+
<p align="center">
|
| 10 |
+
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
| 11 |
+
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
| 12 |
+
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
| 13 |
+
<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>
|
| 14 |
+
<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>
|
| 15 |
+
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
| 16 |
+
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
| 17 |
+
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
| 18 |
+
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
| 19 |
+
<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>
|
| 20 |
+
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
| 21 |
+
</p>
|
| 22 |
+
<!--[](https://opencollective.com/nest#backer)
|
| 23 |
+
[](https://opencollective.com/nest#sponsor)-->
|
| 24 |
+
|
| 25 |
+
## Description
|
| 26 |
+
|
| 27 |
+
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
| 28 |
+
|
| 29 |
+
## Project setup
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
$ npm install
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## Compile and run the project
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
# development
|
| 39 |
+
$ npm run start
|
| 40 |
+
|
| 41 |
+
# watch mode
|
| 42 |
+
$ npm run start:dev
|
| 43 |
+
|
| 44 |
+
# production mode
|
| 45 |
+
$ npm run start:prod
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## Run tests
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
# unit tests
|
| 52 |
+
$ npm run test
|
| 53 |
+
|
| 54 |
+
# e2e tests
|
| 55 |
+
$ npm run test:e2e
|
| 56 |
+
|
| 57 |
+
# test coverage
|
| 58 |
+
$ npm run test:cov
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## Deployment
|
| 62 |
+
|
| 63 |
+
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
| 64 |
+
|
| 65 |
+
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
$ npm install -g mau
|
| 69 |
+
$ mau deploy
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
| 73 |
+
|
| 74 |
+
## Resources
|
| 75 |
+
|
| 76 |
+
Check out a few resources that may come in handy when working with NestJS:
|
| 77 |
+
|
| 78 |
+
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
| 79 |
+
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
| 80 |
+
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
| 81 |
+
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
| 82 |
+
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
| 83 |
+
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
| 84 |
+
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
| 85 |
+
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
| 86 |
+
|
| 87 |
+
## Support
|
| 88 |
+
|
| 89 |
+
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).
|
| 90 |
+
|
| 91 |
+
## Stay in touch
|
| 92 |
+
|
| 93 |
+
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
| 94 |
+
- Website - [https://nestjs.com](https://nestjs.com/)
|
| 95 |
+
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
| 96 |
+
|
| 97 |
+
## License
|
| 98 |
+
|
| 99 |
+
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
backend/nest-cli.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://json.schemastore.org/nest-cli",
|
| 3 |
+
"collection": "@nestjs/schematics",
|
| 4 |
+
"sourceRoot": "src",
|
| 5 |
+
"compilerOptions": {
|
| 6 |
+
"deleteOutDir": true
|
| 7 |
+
}
|
| 8 |
+
}
|
backend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "backend",
|
| 3 |
+
"version": "0.0.1",
|
| 4 |
+
"description": "",
|
| 5 |
+
"author": "",
|
| 6 |
+
"private": true,
|
| 7 |
+
"license": "UNLICENSED",
|
| 8 |
+
"scripts": {
|
| 9 |
+
"build": "nest build",
|
| 10 |
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
| 11 |
+
"start": "nest start",
|
| 12 |
+
"start:dev": "nest start --watch",
|
| 13 |
+
"start:debug": "nest start --debug --watch",
|
| 14 |
+
"start:prod": "node dist/main",
|
| 15 |
+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
| 16 |
+
"test": "jest",
|
| 17 |
+
"test:watch": "jest --watch",
|
| 18 |
+
"test:cov": "jest --coverage",
|
| 19 |
+
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
| 20 |
+
"test:e2e": "jest --config ./test/jest-e2e.json"
|
| 21 |
+
},
|
| 22 |
+
"dependencies": {
|
| 23 |
+
"@nestjs/common": "^10.0.0",
|
| 24 |
+
"@nestjs/config": "^4.0.3",
|
| 25 |
+
"@nestjs/core": "^10.0.0",
|
| 26 |
+
"@nestjs/jwt": "^11.0.2",
|
| 27 |
+
"@nestjs/passport": "^11.0.5",
|
| 28 |
+
"@nestjs/platform-express": "^10.0.0",
|
| 29 |
+
"@nestjs/typeorm": "^11.0.0",
|
| 30 |
+
"bcrypt": "^6.0.0",
|
| 31 |
+
"class-transformer": "^0.5.1",
|
| 32 |
+
"class-validator": "^0.15.1",
|
| 33 |
+
"multer": "^2.1.1",
|
| 34 |
+
"mysql2": "^3.20.0",
|
| 35 |
+
"passport": "^0.7.0",
|
| 36 |
+
"passport-jwt": "^4.0.1",
|
| 37 |
+
"reflect-metadata": "^0.2.0",
|
| 38 |
+
"rxjs": "^7.8.1",
|
| 39 |
+
"sqlite3": "^5.1.7",
|
| 40 |
+
"typeorm": "^0.3.28"
|
| 41 |
+
},
|
| 42 |
+
"devDependencies": {
|
| 43 |
+
"@nestjs/cli": "^10.0.0",
|
| 44 |
+
"@nestjs/schematics": "^10.0.0",
|
| 45 |
+
"@nestjs/testing": "^10.0.0",
|
| 46 |
+
"@types/bcrypt": "^6.0.0",
|
| 47 |
+
"@types/express": "^5.0.0",
|
| 48 |
+
"@types/jest": "^29.5.2",
|
| 49 |
+
"@types/multer": "^2.1.0",
|
| 50 |
+
"@types/node": "^20.3.1",
|
| 51 |
+
"@types/passport-jwt": "^4.0.1",
|
| 52 |
+
"@types/supertest": "^6.0.0",
|
| 53 |
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
| 54 |
+
"@typescript-eslint/parser": "^8.0.0",
|
| 55 |
+
"eslint": "^8.0.0",
|
| 56 |
+
"eslint-config-prettier": "^9.0.0",
|
| 57 |
+
"eslint-plugin-prettier": "^5.0.0",
|
| 58 |
+
"jest": "^29.5.0",
|
| 59 |
+
"prettier": "^3.0.0",
|
| 60 |
+
"source-map-support": "^0.5.21",
|
| 61 |
+
"supertest": "^7.0.0",
|
| 62 |
+
"ts-jest": "^29.1.0",
|
| 63 |
+
"ts-loader": "^9.4.3",
|
| 64 |
+
"ts-node": "^10.9.1",
|
| 65 |
+
"tsconfig-paths": "^4.2.0",
|
| 66 |
+
"typescript": "^5.1.3"
|
| 67 |
+
},
|
| 68 |
+
"jest": {
|
| 69 |
+
"moduleFileExtensions": [
|
| 70 |
+
"js",
|
| 71 |
+
"json",
|
| 72 |
+
"ts"
|
| 73 |
+
],
|
| 74 |
+
"rootDir": "src",
|
| 75 |
+
"testRegex": ".*\\.spec\\.ts$",
|
| 76 |
+
"transform": {
|
| 77 |
+
"^.+\\.(t|j)s$": "ts-jest"
|
| 78 |
+
},
|
| 79 |
+
"collectCoverageFrom": [
|
| 80 |
+
"**/*.(t|j)s"
|
| 81 |
+
],
|
| 82 |
+
"coverageDirectory": "../coverage",
|
| 83 |
+
"testEnvironment": "node"
|
| 84 |
+
}
|
| 85 |
+
}
|
backend/src/admin/admin.controller.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { AdminController } from './admin.controller';
|
| 3 |
+
|
| 4 |
+
describe('AdminController', () => {
|
| 5 |
+
let controller: AdminController;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
controllers: [AdminController],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
controller = module.get<AdminController>(AdminController);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(controller).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
backend/src/admin/admin.controller.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Post, Get, Put, Param, Body, UseGuards } from '@nestjs/common';
|
| 2 |
+
import { AdminService } from './admin.service';
|
| 3 |
+
import { CreateCourseDto } from './dto/create-course.dto';
|
| 4 |
+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
| 5 |
+
import { RolesGuard } from '../auth/guards/roles.guard';
|
| 6 |
+
import { Roles } from '../auth/decorators/roles.decorator';
|
| 7 |
+
import { UserRole } from '../entities/user.entity';
|
| 8 |
+
|
| 9 |
+
@Controller('api/admin')
|
| 10 |
+
@UseGuards(JwtAuthGuard, RolesGuard)
|
| 11 |
+
@Roles(UserRole.ADMIN)
|
| 12 |
+
export class AdminController {
|
| 13 |
+
constructor(private readonly adminService: AdminService) {}
|
| 14 |
+
|
| 15 |
+
@Post('courses')
|
| 16 |
+
async createCourse(@Body() createCourseDto: CreateCourseDto) {
|
| 17 |
+
const data = await this.adminService.createCourse(createCourseDto);
|
| 18 |
+
return { success: true, data };
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@Put('courses/:id')
|
| 22 |
+
async updateCourse(@Param('id') id: string, @Body() updateData: Partial<CreateCourseDto>) {
|
| 23 |
+
const data = await this.adminService.updateCourse(Number(id), updateData);
|
| 24 |
+
return { success: true, data };
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
@Get('orders')
|
| 28 |
+
async getOrders() {
|
| 29 |
+
const data = await this.adminService.getOrders();
|
| 30 |
+
return { success: true, data };
|
| 31 |
+
}
|
| 32 |
+
}
|
backend/src/admin/admin.module.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
| 3 |
+
import { AdminService } from './admin.service';
|
| 4 |
+
import { AdminController } from './admin.controller';
|
| 5 |
+
import { Course } from '../entities/course.entity';
|
| 6 |
+
import { Order } from '../entities/order.entity';
|
| 7 |
+
|
| 8 |
+
@Module({
|
| 9 |
+
imports: [TypeOrmModule.forFeature([Course, Order])],
|
| 10 |
+
providers: [AdminService],
|
| 11 |
+
controllers: [AdminController]
|
| 12 |
+
})
|
| 13 |
+
export class AdminModule {}
|
backend/src/admin/admin.service.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { AdminService } from './admin.service';
|
| 3 |
+
|
| 4 |
+
describe('AdminService', () => {
|
| 5 |
+
let service: AdminService;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
providers: [AdminService],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
service = module.get<AdminService>(AdminService);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(service).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
backend/src/admin/admin.service.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
import { InjectRepository } from '@nestjs/typeorm';
|
| 3 |
+
import { Repository } from 'typeorm';
|
| 4 |
+
import { Course } from '../entities/course.entity';
|
| 5 |
+
import { Order } from '../entities/order.entity';
|
| 6 |
+
import { CreateCourseDto } from './dto/create-course.dto';
|
| 7 |
+
|
| 8 |
+
@Injectable()
|
| 9 |
+
export class AdminService {
|
| 10 |
+
constructor(
|
| 11 |
+
@InjectRepository(Course)
|
| 12 |
+
private courseRepository: Repository<Course>,
|
| 13 |
+
@InjectRepository(Order)
|
| 14 |
+
private orderRepository: Repository<Order>,
|
| 15 |
+
) {}
|
| 16 |
+
|
| 17 |
+
async createCourse(createCourseDto: CreateCourseDto) {
|
| 18 |
+
const course = this.courseRepository.create(createCourseDto);
|
| 19 |
+
return this.courseRepository.save(course);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
async updateCourse(id: number, updateData: Partial<CreateCourseDto>) {
|
| 23 |
+
await this.courseRepository.update(id, updateData);
|
| 24 |
+
return this.courseRepository.findOne({ where: { id } });
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
async getOrders() {
|
| 28 |
+
return this.orderRepository.find({
|
| 29 |
+
relations: ['user', 'course'],
|
| 30 |
+
order: { createdAt: 'DESC' },
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
}
|
backend/src/admin/dto/create-course.dto.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { IsString, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
|
| 2 |
+
|
| 3 |
+
export class CreateCourseDto {
|
| 4 |
+
@IsString()
|
| 5 |
+
@IsNotEmpty()
|
| 6 |
+
title: string;
|
| 7 |
+
|
| 8 |
+
@IsString()
|
| 9 |
+
@IsOptional()
|
| 10 |
+
description?: string;
|
| 11 |
+
|
| 12 |
+
@IsString()
|
| 13 |
+
@IsNotEmpty()
|
| 14 |
+
coverImage: string;
|
| 15 |
+
|
| 16 |
+
@IsString()
|
| 17 |
+
@IsNotEmpty()
|
| 18 |
+
driveLink: string;
|
| 19 |
+
|
| 20 |
+
@IsNumber()
|
| 21 |
+
@IsNotEmpty()
|
| 22 |
+
price: number;
|
| 23 |
+
|
| 24 |
+
@IsString()
|
| 25 |
+
@IsOptional()
|
| 26 |
+
category?: string;
|
| 27 |
+
}
|
backend/src/app.controller.spec.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { AppController } from './app.controller';
|
| 3 |
+
import { AppService } from './app.service';
|
| 4 |
+
|
| 5 |
+
describe('AppController', () => {
|
| 6 |
+
let appController: AppController;
|
| 7 |
+
|
| 8 |
+
beforeEach(async () => {
|
| 9 |
+
const app: TestingModule = await Test.createTestingModule({
|
| 10 |
+
controllers: [AppController],
|
| 11 |
+
providers: [AppService],
|
| 12 |
+
}).compile();
|
| 13 |
+
|
| 14 |
+
appController = app.get<AppController>(AppController);
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
describe('root', () => {
|
| 18 |
+
it('should return "Hello World!"', () => {
|
| 19 |
+
expect(appController.getHello()).toBe('Hello World!');
|
| 20 |
+
});
|
| 21 |
+
});
|
| 22 |
+
});
|
backend/src/app.controller.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Get, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
|
| 2 |
+
import { FileInterceptor } from '@nestjs/platform-express';
|
| 3 |
+
import { AppService } from './app.service';
|
| 4 |
+
import { diskStorage } from 'multer';
|
| 5 |
+
import { extname } from 'path';
|
| 6 |
+
|
| 7 |
+
@Controller()
|
| 8 |
+
export class AppController {
|
| 9 |
+
constructor(private readonly appService: AppService) {}
|
| 10 |
+
|
| 11 |
+
@Get()
|
| 12 |
+
getHello(): string {
|
| 13 |
+
return this.appService.getHello();
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
@Post('api/upload')
|
| 17 |
+
@UseInterceptors(FileInterceptor('file', {
|
| 18 |
+
storage: diskStorage({
|
| 19 |
+
destination: './uploads',
|
| 20 |
+
filename: (req, file, cb) => {
|
| 21 |
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
| 22 |
+
cb(null, `${uniqueSuffix}${extname(file.originalname)}`);
|
| 23 |
+
},
|
| 24 |
+
}),
|
| 25 |
+
}))
|
| 26 |
+
uploadFile(@UploadedFile() file: Express.Multer.File) {
|
| 27 |
+
// Return the URL to access the uploaded file
|
| 28 |
+
// Note: ensure we serve this directory statically
|
| 29 |
+
return {
|
| 30 |
+
success: true,
|
| 31 |
+
url: `/uploads/${file.filename}`,
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
}
|
backend/src/app.module.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
| 3 |
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
| 4 |
+
import { AppController } from './app.controller';
|
| 5 |
+
import { AppService } from './app.service';
|
| 6 |
+
import { User } from './entities/user.entity';
|
| 7 |
+
import { Course } from './entities/course.entity';
|
| 8 |
+
import { Order } from './entities/order.entity';
|
| 9 |
+
import { Payment } from './entities/payment.entity';
|
| 10 |
+
import { UserCourse } from './entities/user-course.entity';
|
| 11 |
+
import { AuthModule } from './auth/auth.module';
|
| 12 |
+
import { CoursesModule } from './courses/courses.module';
|
| 13 |
+
import { OrdersModule } from './orders/orders.module';
|
| 14 |
+
import { AdminModule } from './admin/admin.module';
|
| 15 |
+
import { PaymentModule } from './payment/payment.module';
|
| 16 |
+
|
| 17 |
+
@Module({
|
| 18 |
+
imports: [
|
| 19 |
+
ConfigModule.forRoot({
|
| 20 |
+
isGlobal: true,
|
| 21 |
+
}),
|
| 22 |
+
TypeOrmModule.forRoot({
|
| 23 |
+
type: 'sqlite',
|
| 24 |
+
database: 'course_subscription.sqlite',
|
| 25 |
+
entities: [User, Course, Order, Payment, UserCourse],
|
| 26 |
+
synchronize: true, // Auto-create tables in dev
|
| 27 |
+
}),
|
| 28 |
+
AuthModule,
|
| 29 |
+
CoursesModule,
|
| 30 |
+
OrdersModule,
|
| 31 |
+
AdminModule,
|
| 32 |
+
PaymentModule,
|
| 33 |
+
],
|
| 34 |
+
controllers: [AppController],
|
| 35 |
+
providers: [AppService],
|
| 36 |
+
})
|
| 37 |
+
export class AppModule {}
|
backend/src/app.service.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
|
| 3 |
+
@Injectable()
|
| 4 |
+
export class AppService {
|
| 5 |
+
getHello(): string {
|
| 6 |
+
return 'Hello World!';
|
| 7 |
+
}
|
| 8 |
+
}
|
backend/src/auth/auth.controller.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { AuthController } from './auth.controller';
|
| 3 |
+
|
| 4 |
+
describe('AuthController', () => {
|
| 5 |
+
let controller: AuthController;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
controllers: [AuthController],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
controller = module.get<AuthController>(AuthController);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(controller).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
backend/src/auth/auth.controller.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Post, Body, Get, UseGuards, Request } from '@nestjs/common';
|
| 2 |
+
import { AuthService } from './auth.service';
|
| 3 |
+
import { RegisterDto } from './dto/register.dto';
|
| 4 |
+
import { LoginDto } from './dto/login.dto';
|
| 5 |
+
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
| 6 |
+
|
| 7 |
+
@Controller('api/auth')
|
| 8 |
+
export class AuthController {
|
| 9 |
+
constructor(private readonly authService: AuthService) {}
|
| 10 |
+
|
| 11 |
+
@Post('register')
|
| 12 |
+
async register(@Body() registerDto: RegisterDto) {
|
| 13 |
+
const data = await this.authService.register(registerDto);
|
| 14 |
+
return { success: true, data };
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
@Post('login')
|
| 18 |
+
async login(@Body() loginDto: LoginDto) {
|
| 19 |
+
const data = await this.authService.login(loginDto);
|
| 20 |
+
return { success: true, data };
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@UseGuards(JwtAuthGuard)
|
| 24 |
+
@Get('profile')
|
| 25 |
+
async getProfile(@Request() req) {
|
| 26 |
+
const data = await this.authService.getProfile(req.user.userId);
|
| 27 |
+
return { success: true, data };
|
| 28 |
+
}
|
| 29 |
+
}
|
backend/src/auth/auth.module.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { JwtModule } from '@nestjs/jwt';
|
| 3 |
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
| 4 |
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
| 5 |
+
import { PassportModule } from '@nestjs/passport';
|
| 6 |
+
import { AuthService } from './auth.service';
|
| 7 |
+
import { AuthController } from './auth.controller';
|
| 8 |
+
import { User } from '../entities/user.entity';
|
| 9 |
+
import { JwtStrategy } from './strategies/jwt.strategy';
|
| 10 |
+
|
| 11 |
+
@Module({
|
| 12 |
+
imports: [
|
| 13 |
+
TypeOrmModule.forFeature([User]),
|
| 14 |
+
PassportModule,
|
| 15 |
+
JwtModule.registerAsync({
|
| 16 |
+
imports: [ConfigModule],
|
| 17 |
+
useFactory: async (configService: ConfigService) => ({
|
| 18 |
+
secret: configService.get<string>('JWT_SECRET'),
|
| 19 |
+
signOptions: { expiresIn: '7d' },
|
| 20 |
+
}),
|
| 21 |
+
inject: [ConfigService],
|
| 22 |
+
}),
|
| 23 |
+
],
|
| 24 |
+
providers: [AuthService, JwtStrategy],
|
| 25 |
+
controllers: [AuthController],
|
| 26 |
+
exports: [AuthService],
|
| 27 |
+
})
|
| 28 |
+
export class AuthModule {}
|
backend/src/auth/auth.service.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { AuthService } from './auth.service';
|
| 3 |
+
|
| 4 |
+
describe('AuthService', () => {
|
| 5 |
+
let service: AuthService;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
providers: [AuthService],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
service = module.get<AuthService>(AuthService);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(service).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
backend/src/auth/auth.service.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, UnauthorizedException, BadRequestException, OnModuleInit } from '@nestjs/common';
|
| 2 |
+
import { JwtService } from '@nestjs/jwt';
|
| 3 |
+
import { InjectRepository } from '@nestjs/typeorm';
|
| 4 |
+
import { Repository } from 'typeorm';
|
| 5 |
+
import * as bcrypt from 'bcrypt';
|
| 6 |
+
import { User, UserRole } from '../entities/user.entity';
|
| 7 |
+
import { RegisterDto } from './dto/register.dto';
|
| 8 |
+
import { LoginDto } from './dto/login.dto';
|
| 9 |
+
|
| 10 |
+
@Injectable()
|
| 11 |
+
export class AuthService implements OnModuleInit {
|
| 12 |
+
constructor(
|
| 13 |
+
@InjectRepository(User)
|
| 14 |
+
private userRepository: Repository<User>,
|
| 15 |
+
private jwtService: JwtService,
|
| 16 |
+
) {}
|
| 17 |
+
|
| 18 |
+
async onModuleInit() {
|
| 19 |
+
const adminPhone = '12345678912';
|
| 20 |
+
const adminExists = await this.userRepository.findOne({ where: { phone: adminPhone } });
|
| 21 |
+
if (!adminExists) {
|
| 22 |
+
const salt = await bcrypt.genSalt();
|
| 23 |
+
const passwordHash = await bcrypt.hash('123456', salt);
|
| 24 |
+
const adminUser = this.userRepository.create({
|
| 25 |
+
phone: adminPhone,
|
| 26 |
+
passwordHash,
|
| 27 |
+
nickname: 'Admin',
|
| 28 |
+
role: UserRole.ADMIN,
|
| 29 |
+
});
|
| 30 |
+
await this.userRepository.save(adminUser);
|
| 31 |
+
console.log(`Admin user initialized: ${adminPhone}`);
|
| 32 |
+
} else {
|
| 33 |
+
// Ensure the role is ADMIN and update password if needed
|
| 34 |
+
if (adminExists.role !== UserRole.ADMIN) {
|
| 35 |
+
adminExists.role = UserRole.ADMIN;
|
| 36 |
+
await this.userRepository.save(adminExists);
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
async register(registerDto: RegisterDto) {
|
| 42 |
+
const { phone, password, smsCode } = registerDto;
|
| 43 |
+
|
| 44 |
+
// In a real app, verify smsCode here
|
| 45 |
+
if (smsCode !== '123456') {
|
| 46 |
+
throw new BadRequestException('Invalid SMS code');
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
const existingUser = await this.userRepository.findOne({ where: { phone } });
|
| 50 |
+
if (existingUser) {
|
| 51 |
+
throw new BadRequestException('User already exists');
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const salt = await bcrypt.genSalt();
|
| 55 |
+
const passwordHash = await bcrypt.hash(password, salt);
|
| 56 |
+
|
| 57 |
+
const user = this.userRepository.create({
|
| 58 |
+
phone,
|
| 59 |
+
passwordHash,
|
| 60 |
+
nickname: `User_${phone.slice(-4)}`,
|
| 61 |
+
role: UserRole.USER,
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
await this.userRepository.save(user);
|
| 65 |
+
|
| 66 |
+
return this.login({ phone, password });
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
async login(loginDto: LoginDto) {
|
| 70 |
+
const { phone, password } = loginDto;
|
| 71 |
+
const user = await this.userRepository.findOne({ where: { phone } });
|
| 72 |
+
|
| 73 |
+
if (!user) {
|
| 74 |
+
throw new UnauthorizedException('Invalid credentials');
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const isMatch = await bcrypt.compare(password, user.passwordHash);
|
| 78 |
+
if (!isMatch) {
|
| 79 |
+
throw new UnauthorizedException('Invalid credentials');
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
const payload = { sub: user.id, phone: user.phone, role: user.role };
|
| 83 |
+
return {
|
| 84 |
+
userId: user.id,
|
| 85 |
+
token: this.jwtService.sign(payload),
|
| 86 |
+
role: user.role,
|
| 87 |
+
nickname: user.nickname,
|
| 88 |
+
};
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
async getProfile(userId: number) {
|
| 92 |
+
const user = await this.userRepository.findOne({
|
| 93 |
+
where: { id: userId },
|
| 94 |
+
select: ['id', 'phone', 'email', 'nickname', 'avatar', 'role', 'createdAt']
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
if (!user) {
|
| 98 |
+
throw new UnauthorizedException('User not found');
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return user;
|
| 102 |
+
}
|
| 103 |
+
}
|
backend/src/auth/decorators/roles.decorator.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { SetMetadata } from '@nestjs/common';
|
| 2 |
+
import { UserRole } from '../../entities/user.entity';
|
| 3 |
+
|
| 4 |
+
export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles);
|
backend/src/auth/dto/login.dto.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { IsString, IsNotEmpty } from 'class-validator';
|
| 2 |
+
|
| 3 |
+
export class LoginDto {
|
| 4 |
+
@IsString()
|
| 5 |
+
@IsNotEmpty()
|
| 6 |
+
phone: string;
|
| 7 |
+
|
| 8 |
+
@IsString()
|
| 9 |
+
@IsNotEmpty()
|
| 10 |
+
password: string;
|
| 11 |
+
}
|
backend/src/auth/dto/register.dto.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { IsString, IsNotEmpty, Length } from 'class-validator';
|
| 2 |
+
|
| 3 |
+
export class RegisterDto {
|
| 4 |
+
@IsString()
|
| 5 |
+
@IsNotEmpty()
|
| 6 |
+
@Length(11, 11)
|
| 7 |
+
phone: string;
|
| 8 |
+
|
| 9 |
+
@IsString()
|
| 10 |
+
@IsNotEmpty()
|
| 11 |
+
@Length(6, 20)
|
| 12 |
+
password: string;
|
| 13 |
+
|
| 14 |
+
@IsString()
|
| 15 |
+
@IsNotEmpty()
|
| 16 |
+
smsCode: string;
|
| 17 |
+
}
|
backend/src/auth/guards/jwt-auth.guard.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
import { AuthGuard } from '@nestjs/passport';
|
| 3 |
+
|
| 4 |
+
@Injectable()
|
| 5 |
+
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
backend/src/auth/guards/roles.guard.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
| 2 |
+
import { Reflector } from '@nestjs/core';
|
| 3 |
+
import { UserRole } from '../../entities/user.entity';
|
| 4 |
+
|
| 5 |
+
@Injectable()
|
| 6 |
+
export class RolesGuard implements CanActivate {
|
| 7 |
+
constructor(private reflector: Reflector) {}
|
| 8 |
+
|
| 9 |
+
canActivate(context: ExecutionContext): boolean {
|
| 10 |
+
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>('roles', [
|
| 11 |
+
context.getHandler(),
|
| 12 |
+
context.getClass(),
|
| 13 |
+
]);
|
| 14 |
+
|
| 15 |
+
if (!requiredRoles) {
|
| 16 |
+
return true;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const { user } = context.switchToHttp().getRequest();
|
| 20 |
+
return requiredRoles.some((role) => user?.role === role);
|
| 21 |
+
}
|
| 22 |
+
}
|
backend/src/auth/strategies/jwt.strategy.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ExtractJwt, Strategy } from 'passport-jwt';
|
| 2 |
+
import { PassportStrategy } from '@nestjs/passport';
|
| 3 |
+
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
| 4 |
+
import { ConfigService } from '@nestjs/config';
|
| 5 |
+
import { InjectRepository } from '@nestjs/typeorm';
|
| 6 |
+
import { Repository } from 'typeorm';
|
| 7 |
+
import { User } from '../../entities/user.entity';
|
| 8 |
+
|
| 9 |
+
@Injectable()
|
| 10 |
+
export class JwtStrategy extends PassportStrategy(Strategy) {
|
| 11 |
+
constructor(
|
| 12 |
+
private configService: ConfigService,
|
| 13 |
+
@InjectRepository(User)
|
| 14 |
+
private userRepository: Repository<User>,
|
| 15 |
+
) {
|
| 16 |
+
super({
|
| 17 |
+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
| 18 |
+
ignoreExpiration: false,
|
| 19 |
+
secretOrKey: configService.get<string>('JWT_SECRET') || 'secret',
|
| 20 |
+
});
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async validate(payload: any) {
|
| 24 |
+
const user = await this.userRepository.findOne({ where: { id: payload.sub } });
|
| 25 |
+
if (!user) {
|
| 26 |
+
throw new UnauthorizedException();
|
| 27 |
+
}
|
| 28 |
+
// Return data to be attached to the request object
|
| 29 |
+
return { userId: user.id, phone: user.phone, role: user.role };
|
| 30 |
+
}
|
| 31 |
+
}
|
backend/src/courses/courses.controller.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { CoursesController } from './courses.controller';
|
| 3 |
+
|
| 4 |
+
describe('CoursesController', () => {
|
| 5 |
+
let controller: CoursesController;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
controllers: [CoursesController],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
controller = module.get<CoursesController>(CoursesController);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(controller).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
backend/src/courses/courses.controller.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Get, Param, UseGuards, Request, ParseIntPipe } from '@nestjs/common';
|
| 2 |
+
import { CoursesService } from './courses.service';
|
| 3 |
+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
| 4 |
+
|
| 5 |
+
@Controller('api/courses')
|
| 6 |
+
export class CoursesController {
|
| 7 |
+
constructor(private readonly coursesService: CoursesService) {}
|
| 8 |
+
|
| 9 |
+
@Get()
|
| 10 |
+
async findAll() {
|
| 11 |
+
const data = await this.coursesService.findAll();
|
| 12 |
+
return { success: true, data };
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
@UseGuards(JwtAuthGuard)
|
| 16 |
+
@Get('my')
|
| 17 |
+
async findMyCourses(@Request() req) {
|
| 18 |
+
const data = await this.coursesService.getUserCourses(req.user.userId);
|
| 19 |
+
return { success: true, data };
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
@Get(':id')
|
| 23 |
+
async findOne(@Param('id', ParseIntPipe) id: number) {
|
| 24 |
+
const data = await this.coursesService.findOne(id);
|
| 25 |
+
return { success: true, data };
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
@UseGuards(JwtAuthGuard)
|
| 29 |
+
@Get(':id/access')
|
| 30 |
+
async getAccess(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
| 31 |
+
const data = await this.coursesService.getCourseAccess(req.user.userId, id);
|
| 32 |
+
return { success: true, data };
|
| 33 |
+
}
|
| 34 |
+
}
|
backend/src/courses/courses.module.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
| 3 |
+
import { CoursesService } from './courses.service';
|
| 4 |
+
import { CoursesController } from './courses.controller';
|
| 5 |
+
import { Course } from '../entities/course.entity';
|
| 6 |
+
import { UserCourse } from '../entities/user-course.entity';
|
| 7 |
+
|
| 8 |
+
@Module({
|
| 9 |
+
imports: [TypeOrmModule.forFeature([Course, UserCourse])],
|
| 10 |
+
providers: [CoursesService],
|
| 11 |
+
controllers: [CoursesController],
|
| 12 |
+
exports: [CoursesService],
|
| 13 |
+
})
|
| 14 |
+
export class CoursesModule {}
|
backend/src/courses/courses.service.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { CoursesService } from './courses.service';
|
| 3 |
+
|
| 4 |
+
describe('CoursesService', () => {
|
| 5 |
+
let service: CoursesService;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
providers: [CoursesService],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
service = module.get<CoursesService>(CoursesService);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(service).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
backend/src/courses/courses.service.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
| 2 |
+
import { InjectRepository } from '@nestjs/typeorm';
|
| 3 |
+
import { Repository } from 'typeorm';
|
| 4 |
+
import { Course } from '../entities/course.entity';
|
| 5 |
+
import { UserCourse } from '../entities/user-course.entity';
|
| 6 |
+
|
| 7 |
+
@Injectable()
|
| 8 |
+
export class CoursesService {
|
| 9 |
+
constructor(
|
| 10 |
+
@InjectRepository(Course)
|
| 11 |
+
private courseRepository: Repository<Course>,
|
| 12 |
+
@InjectRepository(UserCourse)
|
| 13 |
+
private userCourseRepository: Repository<UserCourse>,
|
| 14 |
+
) {}
|
| 15 |
+
|
| 16 |
+
async findAll() {
|
| 17 |
+
return this.courseRepository.find({
|
| 18 |
+
where: { isActive: true },
|
| 19 |
+
select: ['id', 'title', 'description', 'coverImage', 'price', 'category'],
|
| 20 |
+
order: { createdAt: 'DESC' },
|
| 21 |
+
});
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
async findOne(id: number) {
|
| 25 |
+
const course = await this.courseRepository.findOne({
|
| 26 |
+
where: { id, isActive: true },
|
| 27 |
+
select: ['id', 'title', 'description', 'coverImage', 'price', 'category'],
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
if (!course) {
|
| 31 |
+
throw new NotFoundException('Course not found');
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return course;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
async getUserCourses(userId: number) {
|
| 38 |
+
const userCourses = await this.userCourseRepository.find({
|
| 39 |
+
where: { userId },
|
| 40 |
+
relations: ['course'],
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
return userCourses.map(uc => ({
|
| 44 |
+
id: uc.course.id,
|
| 45 |
+
title: uc.course.title,
|
| 46 |
+
coverImage: uc.course.coverImage,
|
| 47 |
+
expiredAt: uc.expiredAt,
|
| 48 |
+
}));
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
async getCourseAccess(userId: number, courseId: number) {
|
| 52 |
+
const userCourse = await this.userCourseRepository.findOne({
|
| 53 |
+
where: { userId, courseId },
|
| 54 |
+
relations: ['course'],
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
if (!userCourse) {
|
| 58 |
+
throw new ForbiddenException('You have not purchased this course');
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
if (userCourse.expiredAt < new Date()) {
|
| 62 |
+
throw new ForbiddenException('Your access to this course has expired');
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
return {
|
| 66 |
+
hasAccess: true,
|
| 67 |
+
driveLink: userCourse.course.driveLink, // In a real app, this should be a dynamic signed URL
|
| 68 |
+
expiredAt: userCourse.expiredAt,
|
| 69 |
+
};
|
| 70 |
+
}
|
| 71 |
+
}
|
backend/src/entities/course.entity.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Entity,
|
| 3 |
+
Column,
|
| 4 |
+
PrimaryGeneratedColumn,
|
| 5 |
+
CreateDateColumn,
|
| 6 |
+
UpdateDateColumn,
|
| 7 |
+
OneToMany,
|
| 8 |
+
} from 'typeorm';
|
| 9 |
+
import { Order } from './order.entity';
|
| 10 |
+
import { UserCourse } from './user-course.entity';
|
| 11 |
+
|
| 12 |
+
@Entity('courses')
|
| 13 |
+
export class Course {
|
| 14 |
+
@PrimaryGeneratedColumn()
|
| 15 |
+
id: number;
|
| 16 |
+
|
| 17 |
+
@Column({ type: 'varchar', length: 200 })
|
| 18 |
+
title: string;
|
| 19 |
+
|
| 20 |
+
@Column({ type: 'text', nullable: true })
|
| 21 |
+
description: string;
|
| 22 |
+
|
| 23 |
+
@Column({ type: 'varchar', length: 255, name: 'cover_image' })
|
| 24 |
+
coverImage: string;
|
| 25 |
+
|
| 26 |
+
@Column({ type: 'varchar', length: 500, name: 'drive_link' })
|
| 27 |
+
driveLink: string;
|
| 28 |
+
|
| 29 |
+
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
| 30 |
+
price: number;
|
| 31 |
+
|
| 32 |
+
@Column({ type: 'varchar', length: 50, nullable: true })
|
| 33 |
+
category: string;
|
| 34 |
+
|
| 35 |
+
@Column({ type: 'boolean', default: true, name: 'is_active' })
|
| 36 |
+
isActive: boolean;
|
| 37 |
+
|
| 38 |
+
@CreateDateColumn({ name: 'created_at' })
|
| 39 |
+
createdAt: Date;
|
| 40 |
+
|
| 41 |
+
@UpdateDateColumn({ name: 'updated_at' })
|
| 42 |
+
updatedAt: Date;
|
| 43 |
+
|
| 44 |
+
@OneToMany(() => Order, (order) => order.course)
|
| 45 |
+
orders: Order[];
|
| 46 |
+
|
| 47 |
+
@OneToMany(() => UserCourse, (userCourse) => userCourse.course)
|
| 48 |
+
userCourses: UserCourse[];
|
| 49 |
+
}
|
backend/src/entities/order.entity.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Entity,
|
| 3 |
+
Column,
|
| 4 |
+
PrimaryGeneratedColumn,
|
| 5 |
+
CreateDateColumn,
|
| 6 |
+
ManyToOne,
|
| 7 |
+
JoinColumn,
|
| 8 |
+
OneToMany,
|
| 9 |
+
} from 'typeorm';
|
| 10 |
+
import { User } from './user.entity';
|
| 11 |
+
import { Course } from './course.entity';
|
| 12 |
+
import { Payment } from './payment.entity';
|
| 13 |
+
|
| 14 |
+
export enum OrderStatus {
|
| 15 |
+
PENDING = 'pending',
|
| 16 |
+
PAID = 'paid',
|
| 17 |
+
CANCELLED = 'cancelled',
|
| 18 |
+
REFUNDED = 'refunded',
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@Entity('orders')
|
| 22 |
+
export class Order {
|
| 23 |
+
@PrimaryGeneratedColumn()
|
| 24 |
+
id: number;
|
| 25 |
+
|
| 26 |
+
@Column({ type: 'varchar', length: 32, unique: true, name: 'order_no' })
|
| 27 |
+
orderNo: string;
|
| 28 |
+
|
| 29 |
+
@Column({ name: 'user_id' })
|
| 30 |
+
userId: number;
|
| 31 |
+
|
| 32 |
+
@Column({ name: 'course_id' })
|
| 33 |
+
courseId: number;
|
| 34 |
+
|
| 35 |
+
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
| 36 |
+
amount: number;
|
| 37 |
+
|
| 38 |
+
@Column({ type: 'varchar', default: OrderStatus.PENDING })
|
| 39 |
+
status: OrderStatus;
|
| 40 |
+
|
| 41 |
+
@CreateDateColumn({ name: 'created_at' })
|
| 42 |
+
createdAt: Date;
|
| 43 |
+
|
| 44 |
+
@Column({ type: 'datetime', nullable: true, name: 'paid_at' })
|
| 45 |
+
paidAt: Date;
|
| 46 |
+
|
| 47 |
+
@ManyToOne(() => User, (user) => user.orders)
|
| 48 |
+
@JoinColumn({ name: 'user_id' })
|
| 49 |
+
user: User;
|
| 50 |
+
|
| 51 |
+
@ManyToOne(() => Course, (course) => course.orders)
|
| 52 |
+
@JoinColumn({ name: 'course_id' })
|
| 53 |
+
course: Course;
|
| 54 |
+
|
| 55 |
+
@OneToMany(() => Payment, (payment) => payment.order)
|
| 56 |
+
payments: Payment[];
|
| 57 |
+
}
|
backend/src/entities/payment.entity.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Entity,
|
| 3 |
+
Column,
|
| 4 |
+
PrimaryGeneratedColumn,
|
| 5 |
+
CreateDateColumn,
|
| 6 |
+
ManyToOne,
|
| 7 |
+
JoinColumn,
|
| 8 |
+
} from 'typeorm';
|
| 9 |
+
import { Order } from './order.entity';
|
| 10 |
+
|
| 11 |
+
export enum PayType {
|
| 12 |
+
WECHAT = 'wechat',
|
| 13 |
+
ALIPAY = 'alipay',
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export enum PaymentStatus {
|
| 17 |
+
PENDING = 'pending',
|
| 18 |
+
SUCCESS = 'success',
|
| 19 |
+
FAILED = 'failed',
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
@Entity('payments')
|
| 23 |
+
export class Payment {
|
| 24 |
+
@PrimaryGeneratedColumn()
|
| 25 |
+
id: number;
|
| 26 |
+
|
| 27 |
+
@Column({ name: 'order_id' })
|
| 28 |
+
orderId: number;
|
| 29 |
+
|
| 30 |
+
@Column({ type: 'varchar', length: 64, nullable: true, name: 'payment_no' })
|
| 31 |
+
paymentNo: string;
|
| 32 |
+
|
| 33 |
+
@Column({ type: 'varchar', name: 'pay_type' })
|
| 34 |
+
payType: PayType;
|
| 35 |
+
|
| 36 |
+
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
| 37 |
+
amount: number;
|
| 38 |
+
|
| 39 |
+
@Column({ type: 'varchar', default: PaymentStatus.PENDING })
|
| 40 |
+
status: PaymentStatus;
|
| 41 |
+
|
| 42 |
+
@Column({ type: 'simple-json', nullable: true, name: 'callback_data' })
|
| 43 |
+
callbackData: any;
|
| 44 |
+
|
| 45 |
+
@CreateDateColumn({ name: 'created_at' })
|
| 46 |
+
createdAt: Date;
|
| 47 |
+
|
| 48 |
+
@Column({ type: 'datetime', nullable: true, name: 'completed_at' })
|
| 49 |
+
completedAt: Date;
|
| 50 |
+
|
| 51 |
+
@ManyToOne(() => Order, (order) => order.payments)
|
| 52 |
+
@JoinColumn({ name: 'order_id' })
|
| 53 |
+
order: Order;
|
| 54 |
+
}
|
backend/src/entities/user-course.entity.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Entity,
|
| 3 |
+
Column,
|
| 4 |
+
PrimaryGeneratedColumn,
|
| 5 |
+
CreateDateColumn,
|
| 6 |
+
ManyToOne,
|
| 7 |
+
JoinColumn,
|
| 8 |
+
Index,
|
| 9 |
+
} from 'typeorm';
|
| 10 |
+
import { User } from './user.entity';
|
| 11 |
+
import { Course } from './course.entity';
|
| 12 |
+
|
| 13 |
+
@Entity('user_courses')
|
| 14 |
+
@Index(['userId', 'courseId'], { unique: true })
|
| 15 |
+
export class UserCourse {
|
| 16 |
+
@PrimaryGeneratedColumn()
|
| 17 |
+
id: number;
|
| 18 |
+
|
| 19 |
+
@Column({ name: 'user_id' })
|
| 20 |
+
userId: number;
|
| 21 |
+
|
| 22 |
+
@Column({ name: 'course_id' })
|
| 23 |
+
courseId: number;
|
| 24 |
+
|
| 25 |
+
@Column({ type: 'varchar', length: 64, unique: true, name: 'access_token' })
|
| 26 |
+
accessToken: string;
|
| 27 |
+
|
| 28 |
+
@Column({ type: 'datetime', name: 'expired_at' })
|
| 29 |
+
expiredAt: Date;
|
| 30 |
+
|
| 31 |
+
@CreateDateColumn({ name: 'created_at' })
|
| 32 |
+
createdAt: Date;
|
| 33 |
+
|
| 34 |
+
@ManyToOne(() => User, (user) => user.userCourses)
|
| 35 |
+
@JoinColumn({ name: 'user_id' })
|
| 36 |
+
user: User;
|
| 37 |
+
|
| 38 |
+
@ManyToOne(() => Course, (course) => course.userCourses)
|
| 39 |
+
@JoinColumn({ name: 'course_id' })
|
| 40 |
+
course: Course;
|
| 41 |
+
}
|
backend/src/entities/user.entity.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Entity,
|
| 3 |
+
Column,
|
| 4 |
+
PrimaryGeneratedColumn,
|
| 5 |
+
CreateDateColumn,
|
| 6 |
+
UpdateDateColumn,
|
| 7 |
+
OneToMany,
|
| 8 |
+
} from 'typeorm';
|
| 9 |
+
import { Order } from './order.entity';
|
| 10 |
+
import { UserCourse } from './user-course.entity';
|
| 11 |
+
|
| 12 |
+
export enum UserRole {
|
| 13 |
+
USER = 'user',
|
| 14 |
+
ADMIN = 'admin',
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
@Entity('users')
|
| 18 |
+
export class User {
|
| 19 |
+
@PrimaryGeneratedColumn()
|
| 20 |
+
id: number;
|
| 21 |
+
|
| 22 |
+
@Column({ type: 'varchar', length: 11, unique: true })
|
| 23 |
+
phone: string;
|
| 24 |
+
|
| 25 |
+
@Column({ type: 'varchar', length: 100, unique: true, nullable: true })
|
| 26 |
+
email: string;
|
| 27 |
+
|
| 28 |
+
@Column({ type: 'varchar', length: 255, name: 'password_hash' })
|
| 29 |
+
passwordHash: string;
|
| 30 |
+
|
| 31 |
+
@Column({ type: 'varchar', length: 50 })
|
| 32 |
+
nickname: string;
|
| 33 |
+
|
| 34 |
+
@Column({ type: 'varchar', length: 255, nullable: true })
|
| 35 |
+
avatar: string;
|
| 36 |
+
|
| 37 |
+
@Column({ type: 'varchar', default: UserRole.USER })
|
| 38 |
+
role: UserRole;
|
| 39 |
+
|
| 40 |
+
@CreateDateColumn({ name: 'created_at' })
|
| 41 |
+
createdAt: Date;
|
| 42 |
+
|
| 43 |
+
@UpdateDateColumn({ name: 'updated_at' })
|
| 44 |
+
updatedAt: Date;
|
| 45 |
+
|
| 46 |
+
@OneToMany(() => Order, (order) => order.user)
|
| 47 |
+
orders: Order[];
|
| 48 |
+
|
| 49 |
+
@OneToMany(() => UserCourse, (userCourse) => userCourse.user)
|
| 50 |
+
userCourses: UserCourse[];
|
| 51 |
+
}
|
backend/src/main.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NestFactory } from '@nestjs/core';
|
| 2 |
+
import { ValidationPipe } from '@nestjs/common';
|
| 3 |
+
import { NestExpressApplication } from '@nestjs/platform-express';
|
| 4 |
+
import { join } from 'path';
|
| 5 |
+
import { AppModule } from './app.module';
|
| 6 |
+
|
| 7 |
+
async function bootstrap() {
|
| 8 |
+
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
| 9 |
+
app.enableCors();
|
| 10 |
+
app.useGlobalPipes(new ValidationPipe());
|
| 11 |
+
|
| 12 |
+
// Serve static files from the uploads directory
|
| 13 |
+
app.useStaticAssets(join(__dirname, '..', 'uploads'), {
|
| 14 |
+
prefix: '/uploads/',
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
await app.listen(process.env.PORT ?? 3001);
|
| 18 |
+
}
|
| 19 |
+
bootstrap();
|
| 20 |
+
|
backend/src/orders/dto/create-order.dto.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { IsNumber, IsNotEmpty } from 'class-validator';
|
| 2 |
+
|
| 3 |
+
export class CreateOrderDto {
|
| 4 |
+
@IsNumber()
|
| 5 |
+
@IsNotEmpty()
|
| 6 |
+
courseId: number;
|
| 7 |
+
|
| 8 |
+
@IsNumber()
|
| 9 |
+
@IsNotEmpty()
|
| 10 |
+
price: number;
|
| 11 |
+
}
|
backend/src/orders/orders.controller.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { OrdersController } from './orders.controller';
|
| 3 |
+
|
| 4 |
+
describe('OrdersController', () => {
|
| 5 |
+
let controller: OrdersController;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
controllers: [OrdersController],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
controller = module.get<OrdersController>(OrdersController);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(controller).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
backend/src/orders/orders.controller.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Controller, Post, Body, Get, Param, UseGuards, Request, ParseIntPipe } from '@nestjs/common';
|
| 2 |
+
import { OrdersService } from './orders.service';
|
| 3 |
+
import { CreateOrderDto } from './dto/create-order.dto';
|
| 4 |
+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
| 5 |
+
|
| 6 |
+
@Controller('api/orders')
|
| 7 |
+
@UseGuards(JwtAuthGuard)
|
| 8 |
+
export class OrdersController {
|
| 9 |
+
constructor(private readonly ordersService: OrdersService) {}
|
| 10 |
+
|
| 11 |
+
@Post('create')
|
| 12 |
+
async create(@Request() req, @Body() createOrderDto: CreateOrderDto) {
|
| 13 |
+
const data = await this.ordersService.create(req.user.userId, createOrderDto);
|
| 14 |
+
return { success: true, data };
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
@Get()
|
| 18 |
+
async findAll(@Request() req) {
|
| 19 |
+
const data = await this.ordersService.findUserOrders(req.user.userId);
|
| 20 |
+
return { success: true, data };
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@Get(':id')
|
| 24 |
+
async findOne(@Param('id', ParseIntPipe) id: number, @Request() req) {
|
| 25 |
+
const data = await this.ordersService.findOne(id, req.user.userId);
|
| 26 |
+
return { success: true, data };
|
| 27 |
+
}
|
| 28 |
+
}
|
backend/src/orders/orders.module.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
| 3 |
+
import { OrdersService } from './orders.service';
|
| 4 |
+
import { OrdersController } from './orders.controller';
|
| 5 |
+
import { Order } from '../entities/order.entity';
|
| 6 |
+
import { Course } from '../entities/course.entity';
|
| 7 |
+
|
| 8 |
+
@Module({
|
| 9 |
+
imports: [TypeOrmModule.forFeature([Order, Course])],
|
| 10 |
+
providers: [OrdersService],
|
| 11 |
+
controllers: [OrdersController],
|
| 12 |
+
exports: [OrdersService],
|
| 13 |
+
})
|
| 14 |
+
export class OrdersModule {}
|
backend/src/orders/orders.service.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { OrdersService } from './orders.service';
|
| 3 |
+
|
| 4 |
+
describe('OrdersService', () => {
|
| 5 |
+
let service: OrdersService;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
providers: [OrdersService],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
service = module.get<OrdersService>(OrdersService);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(service).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|
backend/src/orders/orders.service.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
| 2 |
+
import { InjectRepository } from '@nestjs/typeorm';
|
| 3 |
+
import { Repository } from 'typeorm';
|
| 4 |
+
import { Order, OrderStatus } from '../entities/order.entity';
|
| 5 |
+
import { Course } from '../entities/course.entity';
|
| 6 |
+
import { CreateOrderDto } from './dto/create-order.dto';
|
| 7 |
+
|
| 8 |
+
@Injectable()
|
| 9 |
+
export class OrdersService {
|
| 10 |
+
constructor(
|
| 11 |
+
@InjectRepository(Order)
|
| 12 |
+
private orderRepository: Repository<Order>,
|
| 13 |
+
@InjectRepository(Course)
|
| 14 |
+
private courseRepository: Repository<Course>,
|
| 15 |
+
) {}
|
| 16 |
+
|
| 17 |
+
async create(userId: number, createOrderDto: CreateOrderDto) {
|
| 18 |
+
const course = await this.courseRepository.findOne({ where: { id: createOrderDto.courseId } });
|
| 19 |
+
if (!course) {
|
| 20 |
+
throw new NotFoundException('Course not found');
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
if (Number(course.price) !== Number(createOrderDto.price)) {
|
| 24 |
+
throw new BadRequestException('Price mismatch');
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// Generate simple order number
|
| 28 |
+
const orderNo = `${new Date().getFullYear()}${(new Date().getMonth() + 1).toString().padStart(2, '0')}${new Date().getDate().toString().padStart(2, '0')}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`;
|
| 29 |
+
|
| 30 |
+
const order = this.orderRepository.create({
|
| 31 |
+
orderNo,
|
| 32 |
+
userId,
|
| 33 |
+
courseId: course.id,
|
| 34 |
+
amount: course.price,
|
| 35 |
+
status: OrderStatus.PENDING,
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
await this.orderRepository.save(order);
|
| 39 |
+
|
| 40 |
+
return {
|
| 41 |
+
orderId: order.id,
|
| 42 |
+
orderNo: order.orderNo,
|
| 43 |
+
amount: order.amount,
|
| 44 |
+
};
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
async findUserOrders(userId: number) {
|
| 48 |
+
return this.orderRepository.find({
|
| 49 |
+
where: { userId },
|
| 50 |
+
relations: ['course'],
|
| 51 |
+
order: { createdAt: 'DESC' },
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
async findOne(id: number, userId: number) {
|
| 56 |
+
const order = await this.orderRepository.findOne({
|
| 57 |
+
where: { id, userId },
|
| 58 |
+
relations: ['course'],
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
if (!order) {
|
| 62 |
+
throw new NotFoundException('Order not found');
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
return order;
|
| 66 |
+
}
|
| 67 |
+
}
|
backend/src/payment/dto/prepare-payment.dto.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { IsString, IsNotEmpty, IsEnum } from 'class-validator';
|
| 2 |
+
import { PayType } from '../../entities/payment.entity';
|
| 3 |
+
|
| 4 |
+
export class PreparePaymentDto {
|
| 5 |
+
@IsString()
|
| 6 |
+
@IsNotEmpty()
|
| 7 |
+
orderId: string; // Actually number in our system, but PRD said string, we'll convert
|
| 8 |
+
|
| 9 |
+
@IsEnum(PayType)
|
| 10 |
+
@IsNotEmpty()
|
| 11 |
+
payType: PayType;
|
| 12 |
+
}
|
backend/src/payment/payment.controller.spec.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Test, TestingModule } from '@nestjs/testing';
|
| 2 |
+
import { PaymentController } from './payment.controller';
|
| 3 |
+
|
| 4 |
+
describe('PaymentController', () => {
|
| 5 |
+
let controller: PaymentController;
|
| 6 |
+
|
| 7 |
+
beforeEach(async () => {
|
| 8 |
+
const module: TestingModule = await Test.createTestingModule({
|
| 9 |
+
controllers: [PaymentController],
|
| 10 |
+
}).compile();
|
| 11 |
+
|
| 12 |
+
controller = module.get<PaymentController>(PaymentController);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
it('should be defined', () => {
|
| 16 |
+
expect(controller).toBeDefined();
|
| 17 |
+
});
|
| 18 |
+
});
|