trae-bot commited on
Commit
73746a8
·
0 Parent(s):

Prepare Hugging Face Space deployment

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +10 -0
  2. .gitignore +9 -0
  3. Dockerfile +21 -0
  4. backend/.env +7 -0
  5. backend/.eslintrc.js +25 -0
  6. backend/.prettierrc +4 -0
  7. backend/README.md +99 -0
  8. backend/nest-cli.json +8 -0
  9. backend/package-lock.json +0 -0
  10. backend/package.json +85 -0
  11. backend/src/admin/admin.controller.spec.ts +18 -0
  12. backend/src/admin/admin.controller.ts +32 -0
  13. backend/src/admin/admin.module.ts +13 -0
  14. backend/src/admin/admin.service.spec.ts +18 -0
  15. backend/src/admin/admin.service.ts +33 -0
  16. backend/src/admin/dto/create-course.dto.ts +27 -0
  17. backend/src/app.controller.spec.ts +22 -0
  18. backend/src/app.controller.ts +34 -0
  19. backend/src/app.module.ts +37 -0
  20. backend/src/app.service.ts +8 -0
  21. backend/src/auth/auth.controller.spec.ts +18 -0
  22. backend/src/auth/auth.controller.ts +29 -0
  23. backend/src/auth/auth.module.ts +28 -0
  24. backend/src/auth/auth.service.spec.ts +18 -0
  25. backend/src/auth/auth.service.ts +103 -0
  26. backend/src/auth/decorators/roles.decorator.ts +4 -0
  27. backend/src/auth/dto/login.dto.ts +11 -0
  28. backend/src/auth/dto/register.dto.ts +17 -0
  29. backend/src/auth/guards/jwt-auth.guard.ts +5 -0
  30. backend/src/auth/guards/roles.guard.ts +22 -0
  31. backend/src/auth/strategies/jwt.strategy.ts +31 -0
  32. backend/src/courses/courses.controller.spec.ts +18 -0
  33. backend/src/courses/courses.controller.ts +34 -0
  34. backend/src/courses/courses.module.ts +14 -0
  35. backend/src/courses/courses.service.spec.ts +18 -0
  36. backend/src/courses/courses.service.ts +71 -0
  37. backend/src/entities/course.entity.ts +49 -0
  38. backend/src/entities/order.entity.ts +57 -0
  39. backend/src/entities/payment.entity.ts +54 -0
  40. backend/src/entities/user-course.entity.ts +41 -0
  41. backend/src/entities/user.entity.ts +51 -0
  42. backend/src/main.ts +20 -0
  43. backend/src/orders/dto/create-order.dto.ts +11 -0
  44. backend/src/orders/orders.controller.spec.ts +18 -0
  45. backend/src/orders/orders.controller.ts +28 -0
  46. backend/src/orders/orders.module.ts +14 -0
  47. backend/src/orders/orders.service.spec.ts +18 -0
  48. backend/src/orders/orders.service.ts +67 -0
  49. backend/src/payment/dto/prepare-payment.dto.ts +12 -0
  50. 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
+ <!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
23
+ [![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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
+ });