diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..b7fa6fe --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +## 📝 Description + + +## 🔗 Related Issue + +Closes # + +## 🎯 Type of Change +- [ ] 🐛 Bug fix +- [ ] ✨ New feature +- [ ] 🔨 Refactoring +- [ ] 📦 library update +- [ ] 📝 Documentation +- [ ] 🎨 UI/UX + +## ✅ Checklist +- [ ] Code đã được test locally +- [ ] Không có lỗi lint +- [ ] Đã cập nhật documentation (nếu cần) + +## 📸 Screenshots (nếu có) + diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml new file mode 100644 index 0000000..2bdc0b3 --- /dev/null +++ b/.github/workflows/dotnet-ci.yml @@ -0,0 +1,214 @@ +name: .NET CI + +on: + pull_request: + branches: [ dev ] + workflow_dispatch: + +jobs: + code-quality: + name: Code Quality Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Cache NuGet packages + uses: actions/cache@v3 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + run: dotnet restore Project-sem3-backend.sln + + - name: Check code format + run: | + dotnet format Project-sem3-backend.sln --verify-no-changes --verbosity diagnostic + continue-on-error: true + id: format-check + + - name: Format check result + if: steps.format-check.outcome == 'failure' + run: | + echo "::warning::Code formatting issues detected. Run 'dotnet format' locally to fix." + exit 1 + + build: + name: Build & Analyze + runs-on: ubuntu-latest + needs: code-quality + + strategy: + matrix: + configuration: [Debug, Release] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Cache NuGet packages + uses: actions/cache@v3 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + run: dotnet restore Project-sem3-backend.sln + + - name: Build solution (${{ matrix.configuration }}) + run: | + dotnet build Project-sem3-backend.sln \ + --configuration ${{ matrix.configuration }} \ + --no-restore \ + --verbosity normal \ + /p:TreatWarningsAsErrors=true + + - name: Upload build artifacts (${{ matrix.configuration }}) + if: matrix.configuration == 'Release' + uses: actions/upload-artifact@v4 + with: + name: build-artifacts-${{ matrix.configuration }} + path: | + **/bin/${{ matrix.configuration }}/** + **/obj/${{ matrix.configuration }}/** + retention-days: 7 + + test: + name: Run Tests + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Cache NuGet packages + uses: actions/cache@v3 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + run: dotnet restore Project-sem3-backend.sln + + - name: Build for testing + run: dotnet build Project-sem3-backend.sln --configuration Release --no-restore + + - name: Run tests with coverage + run: | + dotnet test Project-sem3-backend.sln \ + --configuration Release \ + --no-build \ + --verbosity normal \ + --logger "trx;LogFileName=test-results.trx" \ + --collect:"XPlat Code Coverage" \ + --results-directory ./TestResults + continue-on-error: true + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: TestResults/**/*.trx + retention-days: 30 + if-no-files-found: ignore + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: TestResults/**/coverage.cobertura.xml + retention-days: 30 + if-no-files-found: ignore + + security-scan: + name: Security Scan + runs-on: ubuntu-latest + needs: code-quality + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore Project-sem3-backend.sln + + - name: Check for vulnerable packages + run: | + dotnet list package --vulnerable --include-transitive 2>&1 | tee vulnerable-packages.txt + if grep -q "has the following vulnerable packages" vulnerable-packages.txt; then + echo "::error::Vulnerable packages detected!" + cat vulnerable-packages.txt + exit 1 + fi + + - name: Check for deprecated packages + run: | + dotnet list package --deprecated 2>&1 | tee deprecated-packages.txt + if grep -q "has the following deprecated packages" deprecated-packages.txt; then + echo "::warning::Deprecated packages detected!" + cat deprecated-packages.txt + fi + continue-on-error: true + + summary: + name: CI Summary + runs-on: ubuntu-latest + needs: [code-quality, build, test, security-scan] + if: always() + + steps: + - name: Check job results + run: | + echo "## CI Pipeline Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Code Quality | ${{ needs.code-quality.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Test | ${{ needs.test.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Security Scan | ${{ needs.security-scan.result }} |" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ needs.code-quality.result }}" == "failure" ]] || \ + [[ "${{ needs.build.result }}" == "failure" ]] || \ + [[ "${{ needs.test.result }}" == "failure" ]] || \ + [[ "${{ needs.security-scan.result }}" == "failure" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ **CI Pipeline Failed**" >> $GITHUB_STEP_SUMMARY + exit 1 + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ **CI Pipeline Passed**" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.gitignore b/.gitignore index 35063fc..73479d9 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,50 @@ CodeCoverage/ # NUnit *.VisualState.xml TestResult.xml -nunit-*.xml \ No newline at end of file +nunit-*.xml + +# ======================== +# Environment / Secrets +# ======================== +appsettings.*.json +!appsettings.*.example.json + +.env +!.env.example +Infrastructure/env/.env* + +# Lock files - uncomment to ignore lock files +# packages.lock.json +# global.json +# ======================== +# OS / IDE +# ======================== +.DS_Store +Thumbs.db +.vs/ +.vscode/ + +# ======================== +# .NET build artifacts +# ======================== +bin/ +obj/ +*.user +*.suo +*.nuget.* +project.assets.json +project.nuget.cache + +# ======================== +# NodeJS +# ======================== +**/node_modules/ +**/.next/ +# ======================== +# Logs +# ======================== +*.log +#========================= +# package-lock root repo +#========================= +/package-lock.json \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..099e190 --- /dev/null +++ b/Program.cs @@ -0,0 +1,18 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.Run(); diff --git a/Project-sem3-backend.csproj b/Project-sem3-backend.csproj new file mode 100644 index 0000000..f05d4eb --- /dev/null +++ b/Project-sem3-backend.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + \ No newline at end of file diff --git a/Project-sem3-backend.http b/Project-sem3-backend.http new file mode 100644 index 0000000..22543a5 --- /dev/null +++ b/Project-sem3-backend.http @@ -0,0 +1,6 @@ +@Project_sem3_backend_HostAddress = http://localhost:5148 + +GET {{Project_sem3_backend_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Project-sem3-backend.sln b/Project-sem3-backend.sln new file mode 100644 index 0000000..9ef39b8 --- /dev/null +++ b/Project-sem3-backend.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project-sem3-backend", "Project-sem3-backend.csproj", "{340EC36E-A46D-4EB3-B3B2-1244FD77A766}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Debug|Any CPU.Build.0 = Debug|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Debug|x64.ActiveCfg = Debug|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Debug|x64.Build.0 = Debug|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Debug|x86.ActiveCfg = Debug|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Debug|x86.Build.0 = Debug|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Release|Any CPU.ActiveCfg = Release|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Release|Any CPU.Build.0 = Release|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Release|x64.ActiveCfg = Release|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Release|x64.Build.0 = Release|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Release|x86.ActiveCfg = Release|Any CPU + {340EC36E-A46D-4EB3-B3B2-1244FD77A766}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..b9e44c2 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5148", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7126;http://localhost:5148", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/appsetting.example.json b/appsetting.example.json new file mode 100644 index 0000000..e7a8cb7 --- /dev/null +++ b/appsetting.example.json @@ -0,0 +1,13 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Port=3306;Database=project_sem3_db;User=devuser;Password=devpass;", + "Redis": "localhost:6379" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..136a7c2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,80 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + container_name: project-sem3-mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: project_sem3_db + MYSQL_USER: devuser + MYSQL_PASSWORD: devpass + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + networks: + - project-network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot"] + interval: 10s + timeout: 5s + retries: 5 + + phpmyadmin: + image: phpmyadmin:latest + container_name: project-sem3-phpmyadmin + restart: always + environment: + PMA_HOST: mysql + PMA_PORT: 3306 + PMA_USER: root + PMA_PASSWORD: root + ports: + - "8080:80" + depends_on: + mysql: + condition: service_healthy + networks: + - project-network + + redis: + image: redis:7-alpine + container_name: project-sem3-redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - project-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + redis-commander: + image: rediscommander/redis-commander:latest + container_name: project-sem3-redis-commander + restart: always + environment: + REDIS_HOSTS: local:redis:6379 + ports: + - "8081:8081" + depends_on: + redis: + condition: service_healthy + networks: + - project-network + +volumes: + mysql_data: + driver: local + redis_data: + driver: local + +networks: + project-network: + driver: bridge diff --git a/global.json b/global.json new file mode 100644 index 0000000..f7fb55b --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.100", + "rollForward": "latestMinor", + "allowPrerelease": false + } +}