Бэкенд:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { SessionSerializer } from './session.serializer';

@Module({
  imports: [UsersModule, PassportModule.register({session: true})],
  providers: [AuthService, LocalStrategy, SessionSerializer]
})
export class AuthModule {}



import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(username: string, password: string) {
    const user = await this.usersService.findOne({ where: { username } });

    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const passwordValid = await bcrypt.compare(password, user.password);

    if (!passwordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    if (user && passwordValid) {
      return {
        userId: user.id,
        username: user.username,
        email: user.email,
        avatarId: user.avatarId
      };
    }

    return null;
  }
}




import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";

@Injectable()
export class AuthenticatedGuard implements CanActivate {
    async canActivate(context: ExecutionContext) {
        const request = context.switchToHttp().getRequest();

        return request.isAuthenticated();
    }
}



import { ExecutionContext, Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { Observable } from "rxjs";

@Injectable()
export class LocalAuthGuard extends AuthGuard('local'){
    async canActivate(context: ExecutionContext) {
        const result = await super.canActivate(context) as boolean
        const request = context.switchToHttp().getRequest();

        await super.logIn(request);

        return result;
    }
}




import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(
        username.toLowerCase(), 
        password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}



import { Injectable } from "@nestjs/common";
import { PassportSerializer } from "@nestjs/passport";

@Injectable()
export class SessionSerializer extends PassportSerializer{
    serializeUser(user: any, done: (err: Error, user: any) => void) {
        done(null, user)
    }


    deserializeUser(payload: any, done: (err: Error, user: any) => void) {
        done(null, payload)
    }
}





import { registerAs } from "@nestjs/config";
import { sqlconfig } from "./sql.config";

export const databaseConfig = registerAs('database',  () => ({
    sql: {
        ...sqlconfig(),
    }
}));






import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { SequelizeModuleOptions, SequelizeOptionsFactory } from "@nestjs/sequelize";
import { User } from "src/users/users.model";

@Injectable()
export class SequelizeConfigService implements SequelizeOptionsFactory{
    constructor (private readonly configService: ConfigService) {}

    createSequelizeOptions(connectionName?: string): SequelizeModuleOptions {
        const {
            sql: { dialect,logging,host,password,username,port,database}
        } = this.configService.get('database');

        return{
            username,
            password,
            dialect,
            logging,
            port,
            host,
            database,
            models: [User],
            autoLoadModels: true,
            synchronize: true,
            define: {
                charset: 'utf8',
                collate: 'utf8_general_ci'
            }
        }
    }
}




import { registerAs } from "@nestjs/config";
import { Dialect } from "sequelize";

export const sqlconfig = registerAs('database', ()=> ({
    dialect: <Dialect>process.env.SQL_DIALECT || 'mysql',
    logging: process.env.SQL_LOGGING === 'true' ? true: false,
    host: process.env.DATABASE_HOST,
    port: +process.env.DATABASE_PORT,
    username: process.env.DATABASE_USER,
    password: process.env.DATABASE_PASSWORD,
    database: process.env.DATABASE_NAME,
    autoLoadEntities: true,
    synchronize: true,
}));





// backend/src/geo/geo.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { GeoService } from './geo.service';

@Controller('geo')
export class GeoController {
  constructor(private readonly geoService: GeoService) {}

  @Get(':layer')
  async getLayer(@Param('layer') layer: string) {
    return this.geoService.getGeoData(layer);
  }

  @Get('details/:id')
  async getFeatureDetails(@Param('id') id: string) {
    return this.geoService.getFeatureDetails(id);
  }
}




// backend/src/geo/geo.module.ts

import { Module } from '@nestjs/common';
import { GeoService } from './geo.service';
import { GeoController } from './geo.controller';

@Module({
  providers: [GeoService],
  controllers: [GeoController],
})
export class GeoModule {}







// backend/src/geo/geo.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';

@Injectable()
export class GeoService {
  private readonly dataPath = path.join(__dirname, '..', '..', 'frontend', 'public', 'data', 'kyrgyzstan');

  async getGeoData(layer: string): Promise<any> {
    const filePath = path.join(this.dataPath, `${layer}.geojson`);
    if (!fs.existsSync(filePath)) {
      throw new NotFoundException(`Layer '${layer}' not found`);
    }

    const data = fs.readFileSync(filePath, 'utf-8');
    return JSON.parse(data);
  }

  async getFeatureDetails(id: string): Promise<any> {
    const layers = ['admin_boundaries', 'mountains', 'rivers', 'waterbodies', 'cities'];

    for (const layer of layers) {
      const data = await this.getGeoData(layer);
      const feature = data.features.find((f: any) => f.properties.id === id);
      if (feature) {
        return feature.properties;
      }
    }

    throw new NotFoundException(`Feature with id '${id}' not found`);
  }
}










export interface TestResult {
    success: boolean;
    totalScore: number;
    maxScore: number;
    percentageScore: string; // e.g., "70.00%"
    detailedResults: DetailedResult[];
  }
  
  export interface DetailedResult {
    question: string;
    type: string;
    userAnswer: any;
    isCorrect: boolean;
    earnedPoints: number;
    maxPoints: number;
  }







// src/testpages/test.controller.ts

import { Controller, Post, Get, Body, UseGuards, Request, Param, BadRequestException } from '@nestjs/common';
import { TestService } from './test.service';
import { AuthenticatedGuard } from '../auth/authenticated.guard';
import { TestResult } from './response.interface'; // Import the interface

@Controller('tests')
export class TestController {
  constructor(private readonly testService: TestService) {}

  // Get available templates
  @Get('/templates')
  async getTemplates() {
    const templates = this.testService.getTemplatesDescriptions();
    console.log('Templates sent to frontend:', templates);
    return { templates };
  }

  // Create a test based on selected templates
  @UseGuards(AuthenticatedGuard)
  @Post('/create')
  async createTest(
    @Request() req,
    @Body() body: { title: string; selectedTemplateIds: string[] },
  ): Promise<{ test?: any; message?: string }> {
    const { title, selectedTemplateIds } = body;
    try {
      const newTest = await this.testService.createTest(req.user.userId, title, selectedTemplateIds);
      console.log('Test created:', newTest);
      return { test: newTest };
    } catch (error) {
      if (error instanceof BadRequestException) {
        return { message: error.message };
      } else {
        console.error('Unexpected error:', error);
        return { message: 'Произошла неизвестная ошибка' };
      }
    }
  }

  // Get user's tests
  @UseGuards(AuthenticatedGuard)
  @Get('/my-tests')
  async getUserTests(@Request() req): Promise<{ tests: any[]; testCount: number }> {
    const tests = await this.testService.findTestsByUser(req.user.userId);
    const testCount = await this.testService.countTestsByUser(req.user.userId);
    console.log(`Fetched ${testCount} tests for user ID ${req.user.userId}`);
    return { tests, testCount };
  }

  // Get a specific test by ID
  @Get('/:id')
  async getTest(@Param('id') id: string) {
    const test = await this.testService.findTestById(id);
    console.log('Fetched test:', test);
    return test;
  }

  // Submit answers to a test
  @UseGuards(AuthenticatedGuard)
  @Post('/:id/submit')
  async submitTest(
    @Param('id') id: string,
    @Body() body: { answers: any[] }
  ): Promise<TestResult> { // Use the TestResult interface
    console.log('Received request to submit test:', id);
    console.log('Request body:', JSON.stringify(body, null, 2));
    try {
      const result = await this.testService.checkTestAnswers(id, body.answers);
      console.log('Test submission result:', result);
      return result;
    } catch (error) {
      if (error instanceof BadRequestException) {
        throw new BadRequestException(error.message);
      } else {
        console.error('Unexpected error:', error);
        throw new BadRequestException('Произошла неизвестная ошибка');
      }
    }
  }
}









// src/testpages/test.model.ts

import { Column, Model, Table, ForeignKey, DataType } from 'sequelize-typescript';
import { User } from '../users/users.model';

@Table
export class Test extends Model {
  @ForeignKey(() => User)
  @Column
  userId: number;

  @Column
  title: string;

  @Column(DataType.JSON)
  questions: any[]; // Убедитесь, что questions хранятся как массив

  @Column(DataType.INTEGER)
  timesCompleted: number;

  @Column(DataType.FLOAT)
  averageScore: number;
}







import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { Test } from './test.model';
import { TestService } from './test.service';
import { TestController } from './test.controller';

@Module({
  imports: [SequelizeModule.forFeature([Test])],
  providers: [TestService],
  controllers: [TestController],
})
export class TestModule {}








// src/testpages/test.service.ts

import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Test } from './test.model';
import templatesData from '../data/templates.json'; // Проверьте корректность пути
import { TestResult, DetailedResult } from './response.interface'; // Импортируйте интерфейсы

interface TestTemplate {
  type: string;
  description: string;
  question: string;
  options?: string[];
  correctOption?: number;
  correctAnswers?: string[];
  pointsPerAnswer?: number;
}

interface Templates {
  [key: string]: TestTemplate;
}

@Injectable()
export class TestService {
  private templates: Templates = templatesData; // Инициализируем шаблоны из JSON
  
  constructor(
    @InjectModel(Test)
    private testModel: typeof Test,
  ) {
    console.log('Templates loaded:', this.templates); // Для отладки
  }

  getTemplatesDescriptions() {
    return Object.entries(this.templates).map(([id, template]) => ({
      id,
      description: template.description,
      type: template.type,
    }));
  }

  async createTest(userId: number, title: string, selectedTemplateIds: string[]) {
    if (selectedTemplateIds.length < 1 || selectedTemplateIds.length > 10) {
      throw new BadRequestException('Тест должен содержать от 1 до 10 вопросов.');
    }

    // Проверяем существование всех выбранных шаблонов
    const selectedTemplates: TestTemplate[] = [];
    for (const templateId of selectedTemplateIds) {
      const template = this.templates[templateId];
      if (!template) {
        throw new BadRequestException(`Шаблон с ID ${templateId} не существует.`);
      }
      selectedTemplates.push(template);
    }

    // Создание массива вопросов
    const questions = selectedTemplates.map(template => {
      if (template.type === 'multiple_choice') {
        return {
          type: 'multiple_choice',
          question: template.question,
          options: template.options,
          correctOption: template.correctOption,
          points: 1, // Назначение 1 балла за вопрос с выбором ответа
        };
      } else if (template.type === 'short_answer') {
        return {
          type: 'short_answer',
          question: template.question,
          correctAnswers: template.correctAnswers,
          points: template.pointsPerAnswer || 1, // Баллы могут варьироваться
        };
      } else {
        throw new BadRequestException(`Неизвестный тип шаблона: ${template.type}`);
      }
    });

    // Добавляем логирование для проверки структуры вопросов
    console.log('Creating test with questions:', questions);

    return this.testModel.create({
      userId,
      title,
      questions: questions,
      timesCompleted: 0,
      averageScore: 0,
    });
  }

  async findTestsByUser(userId: number) {
    const tests = await this.testModel.findAll({ where: { userId } });
    return tests.map(test => ({
      ...test.toJSON(),
    }));
  }

  async findAllTests() {
    const tests = await this.testModel.findAll();
    return tests.map(test => test.toJSON());
  }

  async findTestById(testId: string) {
    const test = await this.testModel.findByPk(testId);
    if (!test) {
      throw new NotFoundException('Тест не найден');
    }
    return test.toJSON();
  }

  async countTestsByUser(userId: number) {
    return this.testModel.count({ where: { userId } });
  }

  async checkTestAnswers(testId: string, userAnswers: any[]): Promise<TestResult> {
    const test = await this.testModel.findByPk(testId);
    if (!test) {
      throw new NotFoundException('Тест не найден');
    }

    const questions = test.questions;

    if (!questions || !Array.isArray(questions)) {
      throw new BadRequestException('Неправильный формат теста');
    }

    if (userAnswers.length !== questions.length) {
      throw new BadRequestException('Некорректное количество ответов');
    }

    let totalScore = 0;
    let maxScore = 0;
    const detailedResults: DetailedResult[] = [];

    for (let i = 0; i < questions.length; i++) {
      const q = questions[i];
      const userAnswer = userAnswers[i];
      let isCorrect = false;
      let earnedPoints = 0;

      if (q.type === 'multiple_choice') {
        if (userAnswer === q.correctOption) {
          isCorrect = true;
          earnedPoints = q.points;
        }
      } else if (q.type === 'short_answer') {
        const normalizedUserAnswer = typeof userAnswer === 'string' ? userAnswer.trim().toLowerCase() : '';
        const isMatch = Array.isArray(q.correctAnswers) && q.correctAnswers.some(ans => ans.toLowerCase() === normalizedUserAnswer);
        if (isMatch) {
          isCorrect = true;
          earnedPoints = q.points;
        }
      }

      totalScore += earnedPoints;
      maxScore += q.points;

      detailedResults.push({
        question: q.question,
        type: q.type,
        userAnswer: userAnswer,
        isCorrect: isCorrect,
        earnedPoints: earnedPoints,
        maxPoints: q.points,
      });
    }

    const rawPercentageScore = maxScore === 0 ? 0 : (totalScore / maxScore) * 100;
    const percentageScoreStr = rawPercentageScore.toFixed(2) + '%';
    const success = rawPercentageScore >= 70; // Проходной балл >= 70%

    // Обновление статистики
    test.timesCompleted += 1;
    test.averageScore = ((test.averageScore * (test.timesCompleted - 1)) + rawPercentageScore) / test.timesCompleted;
    await test.save();

    return {
      success: success,
      totalScore: totalScore,
      maxScore: maxScore,
      percentageScore: percentageScoreStr,
      detailedResults: detailedResults,
    };
  }
}











import { IsNotEmpty } from "class-validator";

export class CreateUserDto {
    @IsNotEmpty()
    readonly username: string;

    @IsNotEmpty()
    readonly password: string;

    @IsNotEmpty()
    readonly email: string;
}









import { Body, Controller, Get, Header, HttpCode, HttpStatus, Post, Request, UseGuards, Patch, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { LocalAuthGuard } from 'src/auth/local.auth.guard';
import { AuthenticatedGuard } from 'src/auth/authenticated.guard';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post('/signup')
  @HttpCode(HttpStatus.CREATED)
  @Header('Content-type', 'application/json')
  async createUser(@Body() CreateUserDto: CreateUserDto) {
    const user = await this.usersService.create(CreateUserDto);
    if ('warningMessage' in user) {
      return user;
    }
    return { userId: user.id, username: user.username, email: user.email, avatarId: user.avatarId };
  }

  @Post('/login')
  @UseGuards(LocalAuthGuard)
  @HttpCode(HttpStatus.OK)
  login(@Request() req) {
    return { user: req.user, msg: 'Logged in successfully' };
  }

  @Get('/login-check')
  @UseGuards(AuthenticatedGuard)
  async loginCheck(@Request() req) {
    const user = await this.usersService.findById(req.user.userId);
    return {
      userId: user.id,
      username: user.username,
      email: user.email,
      avatarId: user.avatarId
    };
  }

  @Get('/logout')
  logout(@Request() req) {
    req.session.destroy();
    return { msg: 'Logged out successfully' };
  }

  @Patch('/:id/avatar')
  @UseGuards(AuthenticatedGuard)
  async updateAvatar(@Param('id') id: string, @Body('avatarId') avatarId: number) {
    const user = await this.usersService.updateAvatar(Number(id), avatarId);
    return { avatarId: user.avatarId };
  }
}










import { Column, Model, Table, DataType } from "sequelize-typescript";

@Table
export class User extends Model {
  @Column
  username: string;
  
  @Column
  password: string;

  @Column
  email: string;

  @Column(DataType.INTEGER)
  avatarId: number;
}








import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users.model';

@Module({
  imports:[SequelizeModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
  exports: [UsersService]
})
export class UsersModule {}










import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './users.model';
import { CreateUserDto } from './dto/create-user.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User)
    private userModel: typeof User,
  ) {}

  findOne(filter: {
    where: { id?: string; username?: string; email?: string };
  }): Promise<User> {
    return this.userModel.findOne({ ...filter });
  }

  async create(
    createUserDto: CreateUserDto,
  ): Promise<User | { warningMessage: string }> {
    const user = new User();
    const existingByUserName = await this.findOne({
      where: { username: createUserDto.username },
    });
    const existingByEmail = await this.findOne({
      where: { email: createUserDto.email },
    });

    if (existingByUserName) {
      return { warningMessage: 'Пользователь с таким именем уже существует' };
    }

    if (existingByEmail) {
      return { warningMessage: 'Пользователь с таким электронным адресом уже существует' };
    }

    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    user.username = createUserDto.username;
    user.password = hashedPassword;
    user.email = createUserDto.email;
    user.avatarId = 1; // Default avatar ID

    return user.save();
  }

  async updateAvatar(userId: number, avatarId: number): Promise<User> {
    const user = await this.userModel.findByPk(userId);
    if (!user) {
      throw new Error('User not found');
    }
    user.avatarId = avatarId;
    await user.save();
    return user;
  }

  async findById(userId: number): Promise<User> {
    const user = await this.userModel.findByPk(userId);
    if (!user) {
      throw new Error('User not found');
    }
    return user;
  }
}











// backend/src/app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { SequelizeModule } from '@nestjs/sequelize';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { TestModule } from './testpages/test.module';
import { GeoModule } from './geo/geo.module';
import { SequelizeConfigService } from './config/sequelizeConfig.service';
import { databaseConfig } from './config/configuration';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [databaseConfig],
      isGlobal: true,
    }),
    SequelizeModule.forRootAsync({
      imports: [ConfigModule],
      useClass: SequelizeConfigService,
      inject: [SequelizeConfigService],
    }),
    UsersModule,
    AuthModule,
    TestModule,
    GeoModule, // Add GeoModule here
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}













import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import session from 'express-session';
import passport from 'passport';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use(
    session({
      secret: 'keyword', // Change this to a strong secret in production
      resave: false,
      saveUninitialized: false,
    }),
  );

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      forbidNonWhitelisted: true,
    }),
  );

  app.use(passport.initialize());
  app.use(passport.session());

  app.enableCors({
    origin: 'http://localhost:3000', // Update if your frontend runs on a different origin
    credentials: true,
  });

  await app.listen(3001);
}
bootstrap();











SQL_DIALECT=mysql
SQL_LOGGING=true
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USER=root
DATABASE_PASSWORD=1369
DATABASE_NAME=test










здесь я использовал mariaDB но хочу перенести на Mysql как это можно сделать