diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index f5cb744..078c79f 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -13,59 +13,77 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 - - name: Create Backend .env file + - name: Setup .NET 8 SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Create Backend appsettings.Production.json run: | - echo "Creating backend .env file..." - cd backend - # Using both Gitea Variables (vars) and Secrets (secrets) - echo "BACKEND_PORT=${{ vars.BACKEND_PORT }}" >> .env - echo "FRONTEND_URL=${{ vars.FRONTEND_URL }}" >> .env - echo "SMTP_HOST=${{ vars.SMTP_HOST }}" >> .env - echo "SMTP_PORT=${{ vars.SMTP_PORT }}" >> .env - echo "SMTP_SECURE=${{ vars.SMTP_SECURE }}" >> .env - echo "YOUR_RECEIVING_EMAIL=${{ vars.YOUR_RECEIVING_EMAIL }}" >> .env - echo "SMTP_FROM_EMAIL=${{ vars.SMTP_FROM_EMAIL }}" >> .env - - # Secrets should be used for sensitive data - echo "SMTP_USER=${{ secrets.SMTP_USER }}" >> .env - echo "SMTP_PASS=${{ secrets.SMTP_PASS }}" >> .env - + echo "Creating backend appsettings.Production.json file..." + # This creates the JSON file by mapping your Gitea secrets/vars + # to the structure you provided. + cat < backend/appsettings.Production.json + { + "SmtpSettings": { + "Host": "${{ vars.SMTP_HOST }}", + "Port": ${{ vars.SMTP_PORT }}, + "User": "${{ secrets.SMTP_USER }}", + "Pass": "${{ secrets.SMTP_PASS }}", + "FromEmail": "${{ vars.SMTP_FROM_EMAIL }}", + "ReceivingEmail": "${{ vars.YOUR_RECEIVING_EMAIL }}" + }, + "CorsOrigins": "${{ vars.FRONTEND_URL }}" + } + EOF - name: Create Frontend .env.local file run: | echo "Creating frontend .env.local file..." cd frontend - # The BACKEND_URL is not needed because Traefik will handle routing /api echo "NEXT_PUBLIC_GITHUB_URL=${{ vars.NEXT_PUBLIC_GITHUB_URL }}" >> .env.local echo "NEXT_PUBLIC_LINKEDIN_URL=${{ vars.NEXT_PUBLIC_LINKEDIN_URL }}" >> .env.local - - name: Cache Dependencies and Build Artifacts + - name: Cache Dependencies uses: actions/cache@v4 with: path: | - backend/node_modules + ~/.nuget/packages frontend/node_modules frontend/.next/cache - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-${{ hashFiles('**/*.csproj') }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | - ${{ runner.os }}-node- + ${{ runner.os }}- - name: Install Dependencies and Build run: | - echo "Installing backend dependencies..." - cd backend && npm install && cd .. + echo "Restoring backend NuGet packages..." + dotnet restore backend + + echo "Building and publishing backend..." + # This compiles the app and places the output in the 'publish' folder + dotnet publish backend --configuration Release --output ./publish + echo "Installing frontend dependencies..." cd frontend && npm install + echo "Building frontend application..." npm run build - name: Sync Files to Production Directory run: | - rsync -a --delete --exclude 'frontend/.next/cache/' --exclude '.git/' ./ /var/www/website.joaoloureiro.dev.br/ + # Sync the published backend from the './publish' directory + rsync -a --delete ./publish/ /var/www/website.joaoloureiro.dev.br/ + + # Sync the built frontend from the 'frontend' directory + rsync -a --delete ./frontend/.next /var/www/website.joaoloureiro.dev.br/ + rsync -a --delete ./frontend/public /var/www/website.joaoloureiro.dev.br/ + rsync -a ./frontend/package.json /var/www/website.joaoloureiro.dev.br/ + rsync -a ./frontend/ecosystem.config.js /var/www/website.joaoloureiro.dev.br/ - name: Restart Applications with PM2 - env: - DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} run: | - restart-portfolio - + # This command on your server should handle restarting both processes + # Ensure your PM2 ecosystem file now has entries for both the .NET app + # and the Next.js app. + restart-portfolio \ No newline at end of file diff --git a/.gitignore b/.gitignore index ceaea36..f84ba3e 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,425 @@ dist .yarn/install-state.gz .pnp.* +# ---> VisualStudio +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +# ---> VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +.idea +backend-node + +*appSettings.Production.json +*appSettings.Staging.json +*appSettings.Development.json diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/backend/JoaoLoureiro.Portfolio.Api/Dockerfile b/backend/JoaoLoureiro.Portfolio.Api/Dockerfile new file mode 100644 index 0000000..6751c4c --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/Dockerfile @@ -0,0 +1,36 @@ +# Stage 1: Build the application +# Use the .NET SDK image which contains all the tools to build and publish +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +# Copy all .csproj files and the .sln file first. This is a key optimization. +# Docker will cache this layer, and will only re-run `dotnet restore` if a project file changes. +COPY ["JoaoLoureiro.Portfolio.Core/JoaoLoureiro.Portfolio.Core.csproj", "JoaoLoureiro.Portfolio.Core/"] +COPY ["JoaoLoureiro.Portfolio.Infrastructure/JoaoLoureiro.Portfolio.Infrastructure.csproj", "JoaoLoureiro.Portfolio.Infrastructure/"] +COPY ["JoaoLoureiro.Portfolio.Api/JoaoLoureiro.Portfolio.Api.csproj", "JoaoLoureiro.Portfolio.Api/"] +COPY ["JoaoLoureiro.Portfolio.sln", "."] + +# Restore all NuGet packages for the entire solution +RUN dotnet restore "JoaoLoureiro.Portfolio.sln" + +# Copy the rest of the source code +COPY . . + +# Publish the API project, creating the release-ready artifacts +WORKDIR "JoaoLoureiro.Portfolio.Api" +RUN dotnet publish "JoaoLoureiro.Portfolio.Api.csproj" -c Release -o /app/publish --no-restore + +# Stage 2: Create the final, minimal runtime image +# Use the lightweight ASP.NET runtime image, which is much smaller than the SDK +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final +WORKDIR /app + +# The new .NET 8 images run as a non-root 'app' user by default for better security. +# We expose port 8080, as the default internal port 80 is often a privileged port. +EXPOSE 8080 + +# Copy the published output from the build stage +COPY --from=build /app/publish . + +# Set the entrypoint for the container +ENTRYPOINT ["dotnet", "JoaoLoureiro.Portfolio.Api.dll"] \ No newline at end of file diff --git a/backend/JoaoLoureiro.Portfolio.Api/Dtos/ContactMessageRequest.cs b/backend/JoaoLoureiro.Portfolio.Api/Dtos/ContactMessageRequest.cs new file mode 100644 index 0000000..1a2e26b --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/Dtos/ContactMessageRequest.cs @@ -0,0 +1,8 @@ +namespace JoaoLoureiro.Portfolio.Api.Dtos; + +public class ContactMessageRequest +{ + public required string Name { get; set; } + public required string Email { get; set; } + public required string Message { get; set; } +} diff --git a/backend/JoaoLoureiro.Portfolio.Api/JoaoLoureiro.Portfolio.Api.csproj b/backend/JoaoLoureiro.Portfolio.Api/JoaoLoureiro.Portfolio.Api.csproj new file mode 100644 index 0000000..bbf8d07 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/JoaoLoureiro.Portfolio.Api.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + 13f24551-c620-43ab-9c94-073e5617c79d + Linux + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/JoaoLoureiro.Portfolio.Api/JoaoLoureiro.Portfolio.Api.http b/backend/JoaoLoureiro.Portfolio.Api/JoaoLoureiro.Portfolio.Api.http new file mode 100644 index 0000000..d38d30e --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/JoaoLoureiro.Portfolio.Api.http @@ -0,0 +1,6 @@ +@JoaoLoureiro.Portfolio.Api_HostAddress = http://localhost:5125 + +GET {{JoaoLoureiro.Portfolio.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/JoaoLoureiro.Portfolio.Api/Mapping/MappingProfile.cs b/backend/JoaoLoureiro.Portfolio.Api/Mapping/MappingProfile.cs new file mode 100644 index 0000000..ceb7dce --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/Mapping/MappingProfile.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using JoaoLoureiro.Portfolio.Api.Dtos; +using JoaoLoureiro.Portfolio.Core.Models; + +namespace JoaoLoureiro.Portfolio.Api.Mapping; + +public class MappingProfile : Profile +{ + public MappingProfile() { + CreateMap() + .ForMember(dest => dest.SenderName, opt => opt.MapFrom(src => src.Name)) + .ForMember(dest => dest.ReplyToEmail, opt => opt.MapFrom(src => src.Email)) + .ForMember(dest => dest.MessageBody, opt => opt.MapFrom(src => src.Message)); + } +} diff --git a/backend/JoaoLoureiro.Portfolio.Api/Program.cs b/backend/JoaoLoureiro.Portfolio.Api/Program.cs new file mode 100644 index 0000000..d4772cf --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/Program.cs @@ -0,0 +1,122 @@ +using AutoMapper; +using FluentValidation; +using HealthChecks.UI.Client; +using JoaoLoureiro.Portfolio.Api.Dtos; +using JoaoLoureiro.Portfolio.Core.Interfaces; +using JoaoLoureiro.Portfolio.Core.Models; +using JoaoLoureiro.Portfolio.Infrastructure.HealthCheck; +using JoaoLoureiro.Portfolio.Infrastructure.Services; +using JoaoLoureiro.Portfolio.Infrastructure.Settings; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc; +using Serilog; +using System.Net; + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseSerilog((context, config) => config.ReadFrom.Configuration(context.Configuration)); + +builder.Services.AddProblemDetails(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var corsOrigins = builder.Configuration["CorsOrigins"]; +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + if (corsOrigins is not null) + { + policy.WithOrigins(corsOrigins.Split(',')) + .AllowAnyHeader() + .AllowAnyMethod(); + } + }); +}); + +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("SmtpSettings")) + .ValidateDataAnnotations() + .ValidateOnStart(); + +builder.Services.AddScoped(); +builder.Services.AddValidatorsFromAssemblyContaining(); + +builder.Services.AddAutoMapper(cfg => { }, typeof(Program)); +builder.Services.AddHealthChecks() + .AddCheck("SMTP"); + +builder.Services.Configure(options => +{ + options.KnownProxies.Add(IPAddress.Parse(builder.Configuration["ProxyIP"] ?? "10.0.10.10")); +}); + +var app = builder.Build(); + +app.UseForwardedHeaders(new ForwardedHeadersOptions +{ + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto +}); + +app.UseSerilogRequestLogging(); +app.UseExceptionHandler(); +app.UseStatusCodePages(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseCors(); + + +app.MapHealthChecks("/health", new HealthCheckOptions +{ + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse +}); + +app.MapPost("/api/email/send", async ( + [FromBody] ContactMessageRequest request, + IValidator validator, + IEmailSender emailSender, + IMapper mapper, + ILogger logger, + CancellationToken cancellationToken) => +{ + var validationResult = await validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + var error = validationResult.Errors.First(); + return Results.BadRequest(new { errorKey = error.ErrorCode }); + } + + try + { + var email = mapper.Map(request); + + await emailSender.SendEmailAsync(email, cancellationToken); + + return Results.Ok(new { message = "Message sent successfully!" }); + } + catch (Exception ex) + { + logger.LogError(ex, "An unexpected error occurred while sending email."); + return Results.Problem( + detail: "An unexpected error occurred on the server.", + statusCode: 500, + title: "Server Error", + extensions: new Dictionary { { "errorKey", "server_unexpected_error" } } + ); + } +}) +.WithName("SendContactMessage") +.WithDescription("Accepts a contact form submission and sends it as an email.") +.Produces(StatusCodes.Status200OK) +.Produces(StatusCodes.Status400BadRequest) +.Produces(StatusCodes.Status500InternalServerError); + + +app.Run(); \ No newline at end of file diff --git a/backend/JoaoLoureiro.Portfolio.Api/Properties/launchSettings.json b/backend/JoaoLoureiro.Portfolio.Api/Properties/launchSettings.json new file mode 100644 index 0000000..e4f4567 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5125" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7153;http://localhost:5125" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36910", + "sslPort": 44372 + } + } +} \ No newline at end of file diff --git a/backend/JoaoLoureiro.Portfolio.Api/Validators/ContactMessageRequestValidator.cs b/backend/JoaoLoureiro.Portfolio.Api/Validators/ContactMessageRequestValidator.cs new file mode 100644 index 0000000..aa1406a --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/Validators/ContactMessageRequestValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using JoaoLoureiro.Portfolio.Api.Dtos; + +namespace JoaoLoureiro.Portfolio.Api.Validators; + +public class ContactMessageRequestValidator : AbstractValidator +{ + public ContactMessageRequestValidator() + { + RuleFor(x => x.Name).NotEmpty().WithErrorCode("status_error_all_fields"); + RuleFor(x => x.Message).NotEmpty().WithErrorCode("status_error_all_fields"); + RuleFor(x => x.Email) + .NotEmpty().WithErrorCode("status_error_all_fields") + .EmailAddress().WithErrorCode("status_error_invalid_email"); + } +} diff --git a/backend/JoaoLoureiro.Portfolio.Api/appsettings.json b/backend/JoaoLoureiro.Portfolio.Api/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Api/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/backend/JoaoLoureiro.Portfolio.Application/Interfaces/IEmailSender.cs b/backend/JoaoLoureiro.Portfolio.Application/Interfaces/IEmailSender.cs new file mode 100644 index 0000000..4ff8ef1 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Application/Interfaces/IEmailSender.cs @@ -0,0 +1,8 @@ +using JoaoLoureiro.Portfolio.Core.Models; + +namespace JoaoLoureiro.Portfolio.Core.Interfaces; + +public interface IEmailSender +{ + Task SendEmailAsync(EmailToSend email, CancellationToken cancellationToken = default); +} diff --git a/backend/JoaoLoureiro.Portfolio.Application/JoaoLoureiro.Portfolio.Core.csproj b/backend/JoaoLoureiro.Portfolio.Application/JoaoLoureiro.Portfolio.Core.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Application/JoaoLoureiro.Portfolio.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/backend/JoaoLoureiro.Portfolio.Application/Models/EmailToSend.cs b/backend/JoaoLoureiro.Portfolio.Application/Models/EmailToSend.cs new file mode 100644 index 0000000..581a166 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Application/Models/EmailToSend.cs @@ -0,0 +1,8 @@ +namespace JoaoLoureiro.Portfolio.Core.Models; + +public class EmailToSend +{ + public required string SenderName { get; set; } + public required string ReplyToEmail { get; set; } + public required string MessageBody { get; set; } +} \ No newline at end of file diff --git a/backend/JoaoLoureiro.Portfolio.Domain/Entities/ContactMessage.cs b/backend/JoaoLoureiro.Portfolio.Domain/Entities/ContactMessage.cs new file mode 100644 index 0000000..db611e0 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Domain/Entities/ContactMessage.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace JoaoLoureiro.Portfolio.Domain.Entities +{ + public class ContactMessage(string name, Email replyToEmail, string message) + { + public string Name { get; private set; } = name; + public Email ReplyToEmail { get; private set; } = replyToEmail; + public string Message { get; private set; } = message; + } +} diff --git a/backend/JoaoLoureiro.Portfolio.Domain/Entities/Email.cs b/backend/JoaoLoureiro.Portfolio.Domain/Entities/Email.cs new file mode 100644 index 0000000..ff6e2cb --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Domain/Entities/Email.cs @@ -0,0 +1,4 @@ +namespace JoaoLoureiro.Portfolio.Domain.Entities; + +public record Email(string Address); + diff --git a/backend/JoaoLoureiro.Portfolio.Domain/JoaoLoureiro.Portfolio.Domain.csproj b/backend/JoaoLoureiro.Portfolio.Domain/JoaoLoureiro.Portfolio.Domain.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Domain/JoaoLoureiro.Portfolio.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/backend/JoaoLoureiro.Portfolio.Infrastructure/HealthCheck/SmtpHealthCheck.cs b/backend/JoaoLoureiro.Portfolio.Infrastructure/HealthCheck/SmtpHealthCheck.cs new file mode 100644 index 0000000..7c2501d --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Infrastructure/HealthCheck/SmtpHealthCheck.cs @@ -0,0 +1,31 @@ + +using JoaoLoureiro.Portfolio.Infrastructure.Settings; +using MailKit.Net.Smtp; +using MailKit.Security; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; + +namespace JoaoLoureiro.Portfolio.Infrastructure.HealthCheck; + +public class SmtpHealthCheck(IOptions smtpSettings) : IHealthCheck +{ + private readonly SmtpSettings _smtpSettings = smtpSettings.Value; + + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + using var client = new SmtpClient(); + try + { + await client.ConnectAsync(_smtpSettings.Host, _smtpSettings.Port, SecureSocketOptions.StartTlsWhenAvailable, cancellationToken); + await client.AuthenticateAsync(_smtpSettings.User, _smtpSettings.Pass, cancellationToken); + await client.DisconnectAsync(true, cancellationToken); + return HealthCheckResult.Healthy("SMTP server is responding correctly."); + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("Failed to connect or authenticate with SMTP server.", exception: ex); + } + } +} \ No newline at end of file diff --git a/backend/JoaoLoureiro.Portfolio.Infrastructure/JoaoLoureiro.Portfolio.Infrastructure.csproj b/backend/JoaoLoureiro.Portfolio.Infrastructure/JoaoLoureiro.Portfolio.Infrastructure.csproj new file mode 100644 index 0000000..cf4d872 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Infrastructure/JoaoLoureiro.Portfolio.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/backend/JoaoLoureiro.Portfolio.Infrastructure/Services/SmtpEmailSender.cs b/backend/JoaoLoureiro.Portfolio.Infrastructure/Services/SmtpEmailSender.cs new file mode 100644 index 0000000..8e15782 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Infrastructure/Services/SmtpEmailSender.cs @@ -0,0 +1,44 @@ +using JoaoLoureiro.Portfolio.Core.Interfaces; +using JoaoLoureiro.Portfolio.Core.Models; +using JoaoLoureiro.Portfolio.Infrastructure.Settings; +using MailKit.Net.Smtp; +using MailKit.Security; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MimeKit; + +namespace JoaoLoureiro.Portfolio.Infrastructure.Services; + +public class SmtpEmailSender(IOptions smtpSettings, ILogger logger) : IEmailSender +{ + private readonly SmtpSettings _smtpSettings = smtpSettings.Value; + private readonly ILogger _logger = logger; + + public async Task SendEmailAsync(EmailToSend email, CancellationToken cancellationToken = default) + { + var mimeMessage = new MimeMessage(); + var fromAddress = _smtpSettings.FromEmail ?? _smtpSettings.User; + + mimeMessage.From.Add(new MailboxAddress(email.SenderName, fromAddress)); + mimeMessage.To.Add(MailboxAddress.Parse(_smtpSettings.ReceivingEmail)); + mimeMessage.ReplyTo.Add(MailboxAddress.Parse(email.ReplyToEmail)); + mimeMessage.Subject = $"New Portfolio Contact: {email.SenderName}"; + + var builder = new BodyBuilder + { + TextBody = $"Name: {email.SenderName}\nEmail: {email.ReplyToEmail}\nMessage: {email.MessageBody}", + HtmlBody = $@"

Name: {email.SenderName}

+

Email: {email.ReplyToEmail}

+

Message:

+

{email.MessageBody.Replace("\n", "
")}

" + }; + mimeMessage.Body = builder.ToMessageBody(); + + using var client = new SmtpClient(); + + await client.ConnectAsync(_smtpSettings.Host, _smtpSettings.Port, SecureSocketOptions.StartTlsWhenAvailable, cancellationToken); + await client.AuthenticateAsync(_smtpSettings.User, _smtpSettings.Pass, cancellationToken); + await client.SendAsync(mimeMessage, cancellationToken); + _logger.LogInformation("Email sent successfully to {Recipient}", _smtpSettings.ReceivingEmail); + } +} diff --git a/backend/JoaoLoureiro.Portfolio.Infrastructure/Settings/SmtpSettings.cs b/backend/JoaoLoureiro.Portfolio.Infrastructure/Settings/SmtpSettings.cs new file mode 100644 index 0000000..c6bc0d2 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.Infrastructure/Settings/SmtpSettings.cs @@ -0,0 +1,11 @@ +namespace JoaoLoureiro.Portfolio.Infrastructure.Settings; + +public class SmtpSettings +{ + public required string Host { get; set; } + public int Port { get; set; } + public required string User { get; set; } + public required string Pass { get; set; } + public string? FromEmail { get; set; } + public required string ReceivingEmail { get; set; } +} diff --git a/backend/JoaoLoureiro.Portfolio.slnx b/backend/JoaoLoureiro.Portfolio.slnx new file mode 100644 index 0000000..e621d45 --- /dev/null +++ b/backend/JoaoLoureiro.Portfolio.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/backend/README.md b/backend/README.md index 1b14f5f..24e2988 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,96 +1,74 @@ -# Portfolio API Backend +# JoaoLoureiro.Portfolio Backend -This is a simple and efficient backend server built with Node.js and Express. Its primary purpose is to handle the contact form submissions from the portfolio frontend, sending emails via SMTP using Nodemailer. +This is the backend for my portfolio application. It is a simple .NET 8 API that handles sending contact messages from the frontend. -## ✨ Features +## Technologies -* **Email Service**: Securely sends emails from the contact form. -* **Input Validation**: Ensures required fields (`name`, `email`, `message`) are present and the email format is valid. -* **CORS Enabled**: Configured with CORS to only accept requests from the frontend application. -* **Specific Error Handling**: Returns structured error keys for different SMTP and server issues, allowing the frontend to display translated, user-friendly messages. -* **Health Check**: Includes a `/api/health` endpoint to easily verify if the server is running. +* .NET 8 +* ASP.NET Core +* Docker +* Serilog for logging +* FluentValidation for request validation +* AutoMapper for object mapping -## 🛠️ Tech Stack +## Project Structure -* **Runtime**: [Node.js](https://nodejs.org/) -* **Framework**: [Express.js](https://expressjs.com/) -* **Email**: [Nodemailer](https://nodemailer.com/) -* **Environment Variables**: [Dotenv](https://github.com/motdotla/dotenv) -* **Cross-Origin Requests**: [CORS](https://github.com/expressjs/cors) +The solution follows a clean architecture pattern: -## 🚀 Getting Started +* `JoaoLoureiro.Portfolio.Api`: The main API project, containing the endpoint for sending emails. +* `JoaoLoureiro.Portfolio.Application`: Contains the core business logic and interfaces. +* `JoaoLoureiro.Portfolio.Domain`: Contains the domain entities. +* `JoaoLoureiro.Portfolio.Infrastructure`: Contains the implementation of services, such as the email sender. + +## Getting Started ### Prerequisites -* Node.js (v18 or later) -* npm +* .NET 8 SDK +* An SMTP server for sending emails. -### Installation & Setup +### Running the application + +1. Clone the repository. +2. Navigate to the `backend` directory. +3. Configure the `SmtpSettings` in `JoaoLoureiro.Portfolio.Api/appsettings.json`. +4. Run the application using the following command: -1. **Navigate to the backend directory:** ```bash - cd backend + dotnet run --project JoaoLoureiro.Portfolio.Api ``` -2. **Install dependencies:** - ```bash - npm install - ``` +## API Endpoints -3. **Create an environment file:** - Create a file named `.env` in the `backend` directory and populate it with your credentials. **Do not commit this file to version control.** +### POST /api/email/send - **.env.example** - ```env - # The port for the backend server - BACKEND_PORT=3001 +This endpoint accepts a contact form submission and sends it as an email. - # Your frontend URL for CORS - FRONTEND_URL=http://localhost:3000 +**Request Body:** - # Nodemailer SMTP Configuration - SMTP_HOST=smtp.example.com - SMTP_PORT=587 - SMTP_SECURE=false - SMTP_USER=your-email@example.com - SMTP_PASS=your-email-password - YOUR_RECEIVING_EMAIL=your-personal-email@example.com - ``` +```json +{ + "name": "John Doe", + "email": "john.doe@example.com", + "message": "Hello, I would like to get in touch with you." +} +``` -4. **Run the server:** - ```bash - npm start - ``` - The server will start on the port defined in your `.env` file (e.g., `3001`). +**Responses:** -## 📝 API Endpoints +* `200 OK`: If the message was sent successfully. +* `400 Bad Request`: If the request is invalid. +* `500 Internal Server Error`: If an unexpected error occurs. -### Health Check +### GET /health -* **GET** `/api/health` - * **Description**: Checks the server status. - * **Success Response (200)**: - ```json - { "status": "UP", "message": "Backend is running" } - ``` +This endpoint returns the health of the application, including the status of the SMTP connection. -### Send Email +## Configuration -* **POST** `/api/email/send` - * **Description**: Processes and sends a contact form submission. - * **Request Body**: - ```json - { - "name": "string", - "email": "string", - "message": "string" - } - ``` - * **Success Response (200)**: - ```json - { "message": "Message sent successfully!" } - ``` - * **Error Responses (4xx/5xx)**: Returns a JSON object with an `errorKey` for the frontend to translate. - ```json - { "errorKey": "smtp_auth_failed" } - ``` \ No newline at end of file +The application is configured using the `appsettings.json` file in the `JoaoLoureiro.Portfolio.Api` project. + +* `SmtpSettings`: Configuration for the SMTP server. +* `CorsOrigins`: A comma-separated list of allowed origins for CORS. +* `ProxyIP`: The IP address of a trusted proxy. +* `Serilog`: Configuration for logging. diff --git a/backend/ecosystem.config.json b/backend/ecosystem.config.json index 69e2d9e..29fbb8e 100644 --- a/backend/ecosystem.config.json +++ b/backend/ecosystem.config.json @@ -1,12 +1,13 @@ { "apps": [{ "name": "portfolio-backend", - "script": "server.js", + "script": "dotnet", + "args": "JoaoLoureiro.Portfolio.Api/bin/Release/net8.0/JoaoLoureiro.Portfolio.Api.dll", "instances": 1, "autorestart": true, "watch": false, "env": { - "NODE_ENV": "production" + "ASPNETCORE_ENVIRONMENT": "Production" } }] } \ No newline at end of file diff --git a/backend/global.json b/backend/global.json new file mode 100644 index 0000000..a11f48e --- /dev/null +++ b/backend/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json deleted file mode 100644 index 4942ac6..0000000 --- a/backend/package-lock.json +++ /dev/null @@ -1,871 +0,0 @@ -{ - "name": "backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "backend", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.5.0", - "express": "^5.1.0", - "nodemailer": "^7.0.3" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nodemailer": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", - "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - } - } -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 43bfea0..0000000 --- a/backend/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "backend", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "start": "node server.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "type": "commonjs", - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.5.0", - "express": "^5.1.0", - "nodemailer": "^7.0.3" - } -} diff --git a/backend/routes/email.js b/backend/routes/email.js deleted file mode 100644 index 931ef56..0000000 --- a/backend/routes/email.js +++ /dev/null @@ -1,58 +0,0 @@ -const express = require('express'); -const nodemailer = require('nodemailer'); -const router = express.Router(); - -router.post('/send', async (req, res) => { - const { name, email, message } = req.body; - - if (!name || !email || !message) { - return res.status(400).json({ errorKey: 'status_error_all_fields' }); - } - if (!/\S+@\S+\.\S+/.test(email)) { - return res.status(400).json({ errorKey: 'status_error_invalid_email' }); - } - - const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: parseInt(process.env.SMTP_PORT || '587', 10), - secure: process.env.SMTP_SECURE === 'true', - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - }, - }); - - const mailOptions = { - from: `"${name}" <${process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER}>`, - replyTo: email, - to: process.env.YOUR_RECEIVING_EMAIL, - subject: `New Portfolio Contact: ${name}`, - text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`, - html: `

Name: ${name}

-

Email: ${email}

-

Message:

-

${message.replace(/\n/g, '
')}

`, - }; - - try { - await transporter.sendMail(mailOptions); - res.status(200).json({ message: 'Message sent successfully!' }); - } catch (error) { - if (error.code) { - switch (error.code) { - case 'EAUTH': - return res.status(500).json({ errorKey: 'smtp_auth_failed' }); - case 'ECONNECTION': - return res.status(500).json({ errorKey: 'smtp_connection_failed' }); - case 'EENVELOPE': - return res.status(400).json({ errorKey: 'smtp_invalid_recipient' }); - default: - return res.status(500).json({ errorKey: 'smtp_generic_error' }); - } - } - - res.status(500).json({ errorKey: 'server_unexpected_error' }); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js deleted file mode 100644 index 8a78cf6..0000000 --- a/backend/server.js +++ /dev/null @@ -1,26 +0,0 @@ -require('dotenv').config(); -const express = require('express'); -const cors = require('cors'); -const emailRoutes = require('./routes/email'); - -const app = express(); -const PORT = process.env.BACKEND_PORT || 3001; - -// Middleware -app.use(cors({ - origin: process.env.FRONTEND_URL || 'http://localhost:3000', // Adjust for your frontend URL -})); -app.use(express.json()); - - - -// Routes -app.use('/api/email', emailRoutes); // All email routes will be prefixed with /api/email - -app.get('/api/health', (req, res) => { // Health check endpoint - res.status(200).json({ status: 'UP', message: 'Backend is running' }); -}); - -app.listen(PORT, () => { - console.log(`Backend server running on port ${PORT}`); -}); \ No newline at end of file