Compare commits

...

2 Commits

Author SHA1 Message Date
7faac8b448 Enhance mailing functionality and update UI titles
- Added `@bounced_email_key` output parameter in `BouncedEmailRepository.cs`.
- Updated dialog title in `BouncedEmailEdit.tsx` from "Add Bounced Email" to "Add Blocked Email".
- Refactored validation schema in `MailingEdit.tsx`, removing old code and adding new validation rules.
- Introduced `nameIsAvailable` and `getNextAvailableName` functions in `MailingEdit.tsx`.
- Integrated `setupData` context in `ActiveMailings.tsx`, `CancelledMailings.tsx`, `NewMailings.tsx`, and `ScheduledMailings.tsx` to refresh setup data during mailing operations.
2025-05-29 15:16:39 -05:00
0e099bfd07 Enhance authentication and logging mechanisms
Updated authentication handling in controllers, added JWT support, and improved error logging. Introduced centralized API calls with customFetch for better token management. Added Grafana's Faro SDK for monitoring and tracing. Refactored project files for improved structure and maintainability.
2025-05-19 17:26:37 -05:00
38 changed files with 2915 additions and 872 deletions

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.API.Controllers; using Surge365.MassEmailReact.API.Controllers;
using Surge365.MassEmailReact.Application.DTOs; using Surge365.MassEmailReact.Application.DTOs;
@ -7,6 +8,7 @@ using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.API.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[AllowAnonymous]
public class AuthenticationController : BaseController public class AuthenticationController : BaseController
{ {
private readonly IAuthService _authService; private readonly IAuthService _authService;
@ -26,6 +28,13 @@ namespace Surge365.MassEmailReact.API.Controllers
SameSite = SameSiteMode.Strict, SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(-1) // Expire immediately Expires = DateTimeOffset.UtcNow.AddDays(-1) // Expire immediately
}); });
Response.Cookies.Append("accessToken", "", new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(-1) // Expire immediately
});
return Ok(new { message = "Logged out successfully" }); return Ok(new { message = "Logged out successfully" });
} }
@ -48,6 +57,7 @@ namespace Surge365.MassEmailReact.API.Controllers
Expires = DateTimeOffset.UtcNow.AddDays(7) Expires = DateTimeOffset.UtcNow.AddDays(7)
}; };
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions); Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
Response.Cookies.Append("accessToken", authResponse.data.Value.accessToken, cookieOptions);
//TODO: Store user in session //TODO: Store user in session
return Ok(new { success = true, authResponse.data.Value.accessToken, authResponse.data.Value.user }); return Ok(new { success = true, authResponse.data.Value.accessToken, authResponse.data.Value.user });
@ -74,6 +84,7 @@ namespace Surge365.MassEmailReact.API.Controllers
Expires = DateTimeOffset.UtcNow.AddDays(7) Expires = DateTimeOffset.UtcNow.AddDays(7)
}; };
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions); Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
Response.Cookies.Append("accessToken", authResponse.data.Value.accessToken, cookieOptions);
return Ok(new { accessToken = authResponse.data.Value.accessToken }); return Ok(new { accessToken = authResponse.data.Value.accessToken });
} }

View File

@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Surge365.MassEmailReact.API.Controllers namespace Surge365.MassEmailReact.API.Controllers
{ {
[Route("[controller]")] [Route("[controller]")]
[Route("api/[controller]")] [Route("api/[controller]")]
[ApiController] [ApiController]
[Authorize]
public class BaseController : ControllerBase public class BaseController : ControllerBase
{ {
} }

View File

@ -6,72 +6,122 @@ using Surge365.MassEmailReact.Infrastructure;
using Surge365.MassEmailReact.Infrastructure.DapperMaps; using Surge365.MassEmailReact.Infrastructure.DapperMaps;
using Surge365.MassEmailReact.Infrastructure.Repositories; using Surge365.MassEmailReact.Infrastructure.Repositories;
using Surge365.MassEmailReact.Infrastructure.Services; using Surge365.MassEmailReact.Infrastructure.Services;
using Surge365.MassEmailReact.Infrastructure.Middleware;
using System.Net; using System.Net;
using System.Security.Authentication; using System.Security.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
WebApplication? app = null;
var keyVaultName = builder.Configuration["KeyVaultName"] ?? ""; try
if (!string.IsNullOrEmpty(keyVaultName))
{ {
var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/"; var keyVaultName = builder.Configuration["KeyVaultName"] ?? "";
builder.Configuration.AddAzureKeyVault( if (!string.IsNullOrEmpty(keyVaultName))
new Uri(keyVaultUri), {
new DefaultAzureCredential(), var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/";
new SurgeKeyVaultSecretManager() builder.Configuration.AddAzureKeyVault(
); new Uri(keyVaultUri),
new DefaultAzureCredential(),
new SurgeKeyVaultSecretManager()
);
}
builder.Services.AddSingleton<ILoggingService, LoggingService>();
var jwtKey = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true, // Enforce expiration
ValidateIssuerSigningKey = true,
//ValidIssuer = "your_issuer", // ADD back if admin api updated to include
//ValidAudience = "your_audience", // ADD back if admin api updated to include
IssuerSigningKey = new SymmetricSecurityKey(jwtKey),
ClockSkew = TimeSpan.Zero // Optional: no grace period for expiration
};
// Read JWT from accessToken cookie
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var token = context.Request.Cookies["accessToken"];
if (!string.IsNullOrEmpty(token))
{
context.Token = token;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddHttpClient("SendGridClient", client =>
{
client.BaseAddress = new Uri("https://api.sendgrid.com/"); // Optional, for clarity
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
});
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<ITargetService, TargetService>();
builder.Services.AddScoped<ITargetRepository, TargetRepository>();
builder.Services.AddScoped<IServerService, ServerService>();
builder.Services.AddScoped<IServerRepository, ServerRepository>();
builder.Services.AddScoped<ITestEmailListService, TestEmailListService>();
builder.Services.AddScoped<ITestEmailListRepository, TestEmailListRepository>();
builder.Services.AddScoped<IBouncedEmailService, BouncedEmailService>();
builder.Services.AddScoped<IBouncedEmailRepository, BouncedEmailRepository>();
builder.Services.AddScoped<IUnsubscribeUrlService, UnsubscribeUrlService>();
builder.Services.AddScoped<IUnsubscribeUrlRepository, UnsubscribeUrlRepository>();
builder.Services.AddScoped<ITemplateService, TemplateService>();
builder.Services.AddScoped<ITemplateRepository, TemplateRepository>();
builder.Services.AddScoped<IEmailDomainService, EmailDomainService>();
builder.Services.AddScoped<IEmailDomainRepository, EmailDomainRepository>();
builder.Services.AddScoped<IMailingService, MailingService>();
builder.Services.AddScoped<IMailingRepository, MailingRepository>();
app = builder.Build();
app.UseCustomExceptionHandler();
app.UseDefaultFiles();
app.MapStaticAssets();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
DapperConfiguration.ConfigureMappings();
} }
catch (Exception ex)
builder.Services.AddHttpClient("SendGridClient", client =>
{ {
client.BaseAddress = new Uri("https://api.sendgrid.com/"); // Optional, for clarity LoggingService appLoggingService = new LoggingService(builder.Configuration);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler appLoggingService.LogError(ex).Wait();
{ return;
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
});
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<ITargetService, TargetService>();
builder.Services.AddScoped<ITargetRepository, TargetRepository>();
builder.Services.AddScoped<IServerService, ServerService>();
builder.Services.AddScoped<IServerRepository, ServerRepository>();
builder.Services.AddScoped<ITestEmailListService, TestEmailListService>();
builder.Services.AddScoped<ITestEmailListRepository, TestEmailListRepository>();
builder.Services.AddScoped<IBouncedEmailService, BouncedEmailService>();
builder.Services.AddScoped<IBouncedEmailRepository, BouncedEmailRepository>();
builder.Services.AddScoped<IUnsubscribeUrlService, UnsubscribeUrlService>();
builder.Services.AddScoped<IUnsubscribeUrlRepository, UnsubscribeUrlRepository>();
builder.Services.AddScoped<ITemplateService, TemplateService>();
builder.Services.AddScoped<ITemplateRepository, TemplateRepository>();
builder.Services.AddScoped<IEmailDomainService, EmailDomainService>();
builder.Services.AddScoped<IEmailDomainRepository, EmailDomainRepository>();
builder.Services.AddScoped<IMailingService, MailingService>();
builder.Services.AddScoped<IMailingRepository, MailingRepository>();
var app = builder.Build();
app.UseDefaultFiles();
app.MapStaticAssets();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
} }
if (app != null)
app.UseHttpsRedirection(); app.Run();
app.UseAuthorization();
app.MapControllers();
DapperConfiguration.ConfigureMappings();
app.Run();

View File

@ -10,6 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SpaProxy"> <PackageReference Include="Microsoft.AspNetCore.SpaProxy">
<Version>9.*-*</Version> <Version>9.*-*</Version>

View File

@ -0,0 +1,21 @@
using Surge365.MassEmailReact.Application.DTOs.AuthApi;
namespace Surge365.MassEmailReact.Application.Interfaces
{
public enum LogLevels //TODO: Move all this to Surge365.Core (new project)
{
Fatal = 1,
Error,
Warn,
Info,
Debug
}
public interface ILoggingService
{
Task<bool> InsertLog(LogLevels level, string logger, int? userKey, string task, string message,
string exceptionStack, string exceptionMessage, string exceptionInnerMessage,
string customMessage1, string customMessage2, string customMessage3,
string customMessage4, string customMessage5);
Task<bool> LogError(Exception ex);
}
}

View File

@ -0,0 +1,66 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Infrastructure;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Middleware
{
public class CustomException
{
private readonly RequestDelegate _next;
private readonly ILoggingService _loggingService;
public CustomException(RequestDelegate next, ILoggingService loggingService)
{
_next = next;
_loggingService = loggingService;
}
/// <summary>
/// Invokes the middleware peforming session start
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch(Exception ex)
{
if (!(ex is ThreadAbortException))
{
/* Guid? loginId = null;
if (context.User.Identity?.IsAuthenticated == true)
{
// Adjust the claim type based on your JWT configuration (e.g., "sub", "nameid", or custom)
var loginIdClaim = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.User.FindFirst("sub")?.Value;
if (!string.IsNullOrEmpty(loginIdClaim) && Guid.TryParse(loginIdClaim, out var parsedUserId))
{
loginId = parsedUserId;
}
}*/
await _loggingService.LogError(ex);
}
throw;
}
}
}
public static class CustomExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomException>();
}
}
}

View File

@ -67,6 +67,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
parameters.Add("@unsubscribe", bouncedEmail.Unsubscribe, DbType.Boolean); parameters.Add("@unsubscribe", bouncedEmail.Unsubscribe, DbType.Boolean);
parameters.Add("@entered_by_admin", bouncedEmail.EnteredByAdmin, DbType.Boolean); parameters.Add("@entered_by_admin", bouncedEmail.EnteredByAdmin, DbType.Boolean);
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
parameters.Add("@bounced_email_key", dbType: DbType.Int32, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_bounced_email", parameters, commandType: CommandType.StoredProcedure); await conn.ExecuteAsync("mem_save_bounced_email", parameters, commandType: CommandType.StoredProcedure);

View File

@ -0,0 +1,66 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Surge365.MassEmailReact.Application.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Infrastructure.Services
{
public class LoggingService(IConfiguration configuration) : ILoggingService
{
private IConfiguration _configuration = configuration;
public async Task<bool> InsertLog(LogLevels level, string logger, int? userId, string task, string message,
string exceptionStack, string exceptionMessage, string exceptionInnerMessage = "",
string customMessage1 = "", string customMessage2 = "", string customMessage3 = "",
string customMessage4 = "", string customMessage5 = "")
{
try
{
string applicationCode = _configuration["AppCode"] ?? "";
List<SqlParameter> pms = new List<SqlParameter> {
new SqlParameter("@ApplicationCode", applicationCode),
new SqlParameter("@level", level),
new SqlParameter("@logger", (string.IsNullOrWhiteSpace(logger) ? this.GetType().Name : logger)),
new SqlParameter("@UserKey", userId),
new SqlParameter("@Task", task),
new SqlParameter("@Message", message),
new SqlParameter("@ExceptionStack", exceptionStack),
new SqlParameter("@ExceptionMessage", exceptionMessage),
new SqlParameter("@ExceptionInnerMessage", exceptionInnerMessage),
new SqlParameter("@CustomMessage1", customMessage1),
new SqlParameter("@CustomMessage2", customMessage2),
new SqlParameter("@CustomMessage3", customMessage3),
new SqlParameter("@CustomMessage4", customMessage4),
new SqlParameter("@CustomMessage5", customMessage5)
};
DataAccess da = new DataAccess(_configuration, "YTBLog.ConnectionString");
await da.CallActionProcedureAsync(pms, "usp_InsertLog");
}
catch
{
return false;
}
return true;
}
public Task<bool> LogError(Exception ex)
{
string exceptionMessage = "";
string exceptionInnerMessage = "";
string exceptionStack = "";
exceptionMessage = ex.Message;
exceptionStack = ex.StackTrace ?? "";
if (ex.InnerException != null)
{
exceptionInnerMessage = ex.InnerException.Message;
}
return InsertLog(LogLevels.Error, "", null, "LogError", ex.Message,
exceptionStack, exceptionMessage, exceptionInnerMessage, "", "", "", "", "");
}
}
}

View File

@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" /> <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.0" />
<PackageReference Include="Dapper.FluentMap" Version="2.0.0" /> <PackageReference Include="Dapper.FluentMap" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" />

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.2191419"> <Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.2191419">
<PropertyGroup> <PropertyGroup>
<StartupCommand>npm run dev</StartupCommand> <StartupCommand>npm run dev</StartupCommand>
<JavaScriptTestRoot>src\</JavaScriptTestRoot> <JavaScriptTestRoot>src\</JavaScriptTestRoot>

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,8 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.1.1", "@fontsource/roboto": "^5.1.1",
"@grafana/faro-web-sdk": "^1.18.0",
"@grafana/faro-web-tracing": "^1.18.0",
"@hookform/resolvers": "^4.1.2", "@hookform/resolvers": "^4.1.2",
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^6.4.5", "@mui/icons-material": "^6.4.5",

View File

@ -1,6 +1,6 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import utils from '@/ts/utils'; import utils from '@/utils/utils';
import { useAuth } from '@/components/auth/AuthContext'; import { useAuth } from '@/components/auth/AuthContext';
const AuthCheck: React.FC = () => { const AuthCheck: React.FC = () => {

View File

@ -1,5 +1,5 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import utils from '@/ts/utils'; import utils from '@/utils/utils';
import { Box, CircularProgress, Typography } from '@mui/material'; import { Box, CircularProgress, Typography } from '@mui/material';
interface AuthContextType { interface AuthContextType {
@ -7,6 +7,7 @@ interface AuthContextType {
userRoles: string[]; userRoles: string[];
setAuth: (token: string | null) => void; setAuth: (token: string | null) => void;
isLoading: boolean; // Add loading state isLoading: boolean; // Add loading state
refreshToken: () => Promise<boolean>;
} }
const AuthContext = createContext<AuthContextType>({ const AuthContext = createContext<AuthContextType>({
@ -14,6 +15,7 @@ const AuthContext = createContext<AuthContextType>({
userRoles: [], userRoles: [],
setAuth: () => { }, setAuth: () => { },
isLoading: true, // Default to loading isLoading: true, // Default to loading
refreshToken: async () => false,
}); });
export const AuthProvider = ({ children }: { children: ReactNode }) => { export const AuthProvider = ({ children }: { children: ReactNode }) => {
@ -33,6 +35,26 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
} }
}; };
const refreshToken = async (): Promise<boolean> => {
try {
const response = await fetch('/api/authentication/refreshtoken', {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setAuth(data.accessToken);
return true;
} else {
setAuth(null);
return false;
}
} catch {
setAuth(null);
return false;
}
};
// Check auth on mount // Check auth on mount
useEffect(() => { useEffect(() => {
const initializeAuth = async () => { const initializeAuth = async () => {
@ -55,9 +77,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
} }
} catch { } catch {
setAuth(null); setAuth(null);
} finally {
setIsLoading(false); // Done loading regardless of outcome
} }
setIsLoading(false); // Done loading regardless of outcome
} }
}; };
@ -65,7 +86,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}, []); }, []);
return ( return (
<AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading }}> <AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading, refreshToken }}>
{isLoading ? ( {isLoading ? (
<Box <Box
sx={{ sx={{

View File

@ -45,6 +45,7 @@ import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl'; import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import { useCustomFetch } from "@/utils/customFetch";
// Constants // Constants
@ -79,6 +80,7 @@ interface LayoutProps {
} }
const Layout = ({ children }: LayoutProps) => { const Layout = ({ children }: LayoutProps) => {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); //TODO: Move this to shared utils? const isMobile = useMediaQuery(theme.breakpoints.down("sm")); //TODO: Move this to shared utils?
const [open, setOpen] = React.useState(!isMobile); const [open, setOpen] = React.useState(!isMobile);
@ -118,7 +120,7 @@ const Layout = ({ children }: LayoutProps) => {
const handleRefreshUser = async () => { const handleRefreshUser = async () => {
handleCloseProfileMenu(); handleCloseProfileMenu();
try { try {
const response = await fetch('/api/authentication/refreshtoken', { const response = await customFetch('/api/authentication/refreshtoken', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
@ -138,7 +140,7 @@ const Layout = ({ children }: LayoutProps) => {
const handleLogout = async () => { const handleLogout = async () => {
setAuth(null); // Clear context setAuth(null); // Clear context
await fetch('/api/authentication/logout', { method: 'POST', credentials: 'include' }); await customFetch('/api/authentication/logout', { method: 'POST', credentials: 'include' });
navigate('/login'); navigate('/login');
}; };

View File

@ -14,6 +14,7 @@ import { useSetupData, SetupData } from "@/context/SetupDataContext";
import { useForm, Resolver, Controller } from "react-hook-form"; import { useForm, Resolver, Controller } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useCustomFetch } from "@/utils/customFetch";
type BouncedEmailEditProps = { type BouncedEmailEditProps = {
open: boolean; open: boolean;
@ -48,6 +49,7 @@ const defaultBouncedEmail: BouncedEmail = {
}; };
const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailEditProps) => { const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailEditProps) => {
const customFetch = useCustomFetch();
const isNew = bouncedEmail == null; const isNew = bouncedEmail == null;
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const originalBouncedEmail: BouncedEmail | null = bouncedEmail ? { ...bouncedEmail } : null; const originalBouncedEmail: BouncedEmail | null = bouncedEmail ? { ...bouncedEmail } : null;
@ -73,7 +75,7 @@ const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailE
const method = isNew ? "POST" : "PUT" const method = isNew ? "POST" : "PUT"
setLoading(true); setLoading(true);
try { try {
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),
@ -93,7 +95,7 @@ const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailE
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>{isNew ? "Add Bounced Email" : "Edit Bounced Email"}</DialogTitle> <DialogTitle>{isNew ? "Add Blocked Email" : "Edit Blocked Email"}</DialogTitle>
<DialogContent> <DialogContent>
<TextField <TextField
{...register("emailAddress")} {...register("emailAddress")}

View File

@ -14,6 +14,7 @@ import { useSetupData, SetupData } from "@/context/SetupDataContext";
import { useForm, Controller, Resolver } from "react-hook-form"; import { useForm, Controller, Resolver } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useCustomFetch } from "@/utils/customFetch";
type EmailDomainEditProps = { type EmailDomainEditProps = {
open: boolean; open: boolean;
@ -56,6 +57,7 @@ const defaultEmailDomain: EmailDomain = {
}; };
const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEditProps) => { const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEditProps) => {
const customFetch = useCustomFetch();
const isNew = !emailDomain || emailDomain.id === 0; const isNew = !emailDomain || emailDomain.id === 0;
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null; const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null;
@ -80,7 +82,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
const method = isNew ? "POST" : "PUT"; const method = isNew ? "POST" : "PUT";
setLoading(true); setLoading(true);
try { try {
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),

View File

@ -1,7 +1,8 @@
import { useState, FormEvent } from 'react'; import { useState, FormEvent } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/material'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material'; import { Close as CloseIcon } from '@mui/icons-material';
import utils from '@/ts/utils'; import utils from '@/utils/utils';
import { useCustomFetch } from "@/utils/customFetch";
type FormErrors = Record<string, string>; type FormErrors = Record<string, string>;
@ -11,6 +12,7 @@ type ForgotPasswordModalProps = {
}; };
const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose }) => { const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose }) => {
const customFetch = useCustomFetch();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [formErrors, setFormErrors] = useState<FormErrors>({}); const [formErrors, setFormErrors] = useState<FormErrors>({});
const [usernameNotFound, setUsernameNotFound] = useState(false); const [usernameNotFound, setUsernameNotFound] = useState(false);
@ -36,7 +38,7 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
if (validate()) { if (validate()) {
console.log('Processing forgot password for', username); console.log('Processing forgot password for', username);
const apiUrl = "/api/authentication/generatepasswordrecovery"; const apiUrl = "/api/authentication/generatepasswordrecovery";
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username}), body: JSON.stringify({ username}),

View File

@ -40,6 +40,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling
import utc from 'dayjs/plugin/utc'; // Import the UTC plugin import utc from 'dayjs/plugin/utc'; // Import the UTC plugin
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useCustomFetch } from "@/utils/customFetch";
dayjs.extend(utc); dayjs.extend(utc);
@ -66,166 +67,8 @@ const recurringTypeOptions = [
{ code: 'W', name: 'Weekly' }, { code: 'W', name: 'Weekly' },
]; ];
const schema = yup.object().shape({
id: yup.number().nullable(),
name: yup.string().required("Name is required")
.test("unique-name", "Name must be unique", async function (value) {
if (value.length === 0)
return true;
return await nameIsAvailable(this.parent.id, value);
}),
description: yup.string().default(""),
templateId: yup.number().typeError("Template is required").required("Template is required").test("valid-template", "Invalid template", function (value) {
const setupData = this.options.context?.setupData as SetupData;
return setupData.templates.some(t => t.id === value);
}),
targetId: yup.number().typeError("Target is required").required("Target is required").test("valid-target", "Invalid target", function (value) {
const setupData = this.options.context?.setupData as SetupData;
return setupData.targets.some(t => t.id === value);
}),
statusCode: yup.string().default("ED"),
scheduleDate: yup.string()
.nullable()
.when("$scheduleForLater", (scheduleForLater, schema) => {
const isScheduledForLater = scheduleForLater[0] ?? false;
return isScheduledForLater
? schema
.required("Schedule date is required when scheduled for later")
.matches(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/,
"Schedule date must be a valid UTC ISO string (e.g., 'YYYY-MM-DDTHH:mm:ssZ')"
)
.test("is-future", "Schedule date must be in the future", (value) => {
if (!value) return true; // Nullable when not required
return dayjs(value).isAfter(dayjs());
})
: schema.nullable();
}),
//scheduleDate: yup.date().nullable(),
// .when("statusCode", {
// is: (value: string) => value === "SC" || value === "SD", // String comparison
// then: (schema) => schema.required("Schedule date is required for scheduled or sending status"),
// otherwise: (schema) => schema.nullable(),
//}),
sentDate: yup.date().nullable().default(null),
sessionActivityId: yup.string().nullable(),
recurringTypeCode: yup
.string()
.nullable()
.when("$recurring", (recurring, schema) => { // Use context variable
const isRecurring = recurring[0] ?? false;
return isRecurring
? schema.oneOf(recurringTypeOptions.map((r) => r.code), "Invalid recurring type")
: schema.nullable();
}),
recurringStartDate: yup.string()
.nullable()
.when("$recurring", (recurring, schema) => {
const isRecurring = recurring[0] ?? false;
return isRecurring
? schema
.required("Recurring start date is required when recurring")
.matches(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/,
"Recurring start date must be a valid UTC ISO string (e.g., 'YYYY-MM-DDTHH:mm:ssZ')"
)
.test("is-future", "Recurring start date must be in the future", (value) => {
if (!value) return true; // Nullable when not required
return dayjs(value).isAfter(dayjs());
})
: schema.nullable();
}),
template: yup.object().shape({
id: yup.number().nullable().default(0),
mailingId: yup.number().default(0),
name: yup.string().default(""),
domainId: yup
.number()
.typeError("Domain is required")
.required("Domain is required")
.test("valid-domain", "Invalid domain", function (value) {
const setupData = this.options.context?.setupData as SetupData;
return setupData.emailDomains.some((d) => d.id === value);
}),
description: yup.string().default(""),
htmlBody: yup.string().default(""),
subject: yup.string().required("Subject is required").default(""),
toName: yup.string().default(""),
fromName: yup.string().required("From Name is required").default(""),
fromEmail: yup.string().default(""),
replyToEmail: yup.string().default(""),
clickTracking: yup.boolean().default(false),
openTracking: yup.boolean().default(false),
categoryXml: yup.string().default(""),
}),
target: yup.object().shape({
id: yup.number().nullable().default(0),
mailingId: yup.number().default(0),
serverId: yup.number().default(0),
name: yup.string().default(""),
databaseName: yup.string().default(""),
viewName: yup.string().default(""),
filterQuery: yup.string().default(""),
allowWriteBack: yup.boolean().default(false),
}).nullable(),
});
const nameIsAvailable = async (id: number, name: string) => {
const response = await fetch(`/api/mailings/available?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
const data = await response.json();
return data.available;
};
const getNextAvailableName = async (id: number, name: string) => {
const response = await fetch(`/api/mailings/nextavailablename?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
const data = await response.json();
return data.name;
};
const defaultMailing: Mailing = {
id: 0,
name: "",
description: "",
templateId: 0,
targetId: 0,
statusCode: "ED",
scheduleDate: null,
sentDate: null,
sessionActivityId: null,
recurringTypeCode: null,
recurringStartDate: null,
template: {
id: 0,
mailingId: 0,
name: "",
domainId: 0,
description: "",
htmlBody: "",
subject: "",
toName: "",
fromName: "",
fromEmail: "",
replyToEmail: "",
clickTracking: false,
openTracking: false,
categoryXml: ""
},
target: {
id: 0,
mailingId: 0,
serverId: 0,
name: "",
databaseName: "",
viewName: "",
filterQuery: "",
allowWriteBack: false,
}
,
};
const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => { const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
const customFetch = useCustomFetch();
const isNew = !mailing || mailing.id === 0; const isNew = !mailing || mailing.id === 0;
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const [approved, setApproved] = useState<boolean>(false); const [approved, setApproved] = useState<boolean>(false);
@ -240,6 +83,166 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
const [targetSample, setTargetSample] = useState<TargetSample | null>(null); const [targetSample, setTargetSample] = useState<TargetSample | null>(null);
const [targetSampleLoading, setTargetSampleLoading] = useState(false); const [targetSampleLoading, setTargetSampleLoading] = useState(false);
const defaultMailing: Mailing = {
id: 0,
name: "",
description: "",
templateId: 0,
targetId: 0,
statusCode: "ED",
scheduleDate: null,
sentDate: null,
sessionActivityId: null,
recurringTypeCode: null,
recurringStartDate: null,
template: {
id: 0,
mailingId: 0,
name: "",
domainId: 0,
description: "",
htmlBody: "",
subject: "",
toName: "",
fromName: "",
fromEmail: "",
replyToEmail: "",
clickTracking: false,
openTracking: false,
categoryXml: ""
},
target: {
id: 0,
mailingId: 0,
serverId: 0,
name: "",
databaseName: "",
viewName: "",
filterQuery: "",
allowWriteBack: false,
}
,
};
const nameIsAvailable = async (id: number, name: string) => {
const response = await customFetch(`/api/mailings/available?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
const data = await response.json();
return data.available;
};
const getNextAvailableName = async (id: number, name: string) => {
const response = await customFetch(`/api/mailings/nextavailablename?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
const data = await response.json();
return data.name;
};
const schema = yup.object().shape({
id: yup.number().nullable(),
name: yup.string().required("Name is required")
.test("unique-name", "Name must be unique", async function (value) {
if (value.length === 0)
return true;
return await nameIsAvailable(this.parent.id, value);
}),
description: yup.string().default(""),
templateId: yup.number().typeError("Template is required").required("Template is required").test("valid-template", "Invalid template", function (value) {
const setupData = this.options.context?.setupData as SetupData;
return setupData.templates.some(t => t.id === value);
}),
targetId: yup.number().typeError("Target is required").required("Target is required").test("valid-target", "Invalid target", function (value) {
const setupData = this.options.context?.setupData as SetupData;
return setupData.targets.some(t => t.id === value);
}),
statusCode: yup.string().default("ED"),
scheduleDate: yup.string()
.nullable()
.when("$scheduleForLater", (scheduleForLater, schema) => {
const isScheduledForLater = scheduleForLater[0] ?? false;
return isScheduledForLater
? schema
.required("Schedule date is required when scheduled for later")
.matches(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/,
"Schedule date must be a valid UTC ISO string (e.g., 'YYYY-MM-DDTHH:mm:ssZ')"
)
.test("is-future", "Schedule date must be in the future", (value) => {
if (!value) return true; // Nullable when not required
return dayjs(value).isAfter(dayjs());
})
: schema.nullable();
}),
//scheduleDate: yup.date().nullable(),
// .when("statusCode", {
// is: (value: string) => value === "SC" || value === "SD", // String comparison
// then: (schema) => schema.required("Schedule date is required for scheduled or sending status"),
// otherwise: (schema) => schema.nullable(),
//}),
sentDate: yup.date().nullable().default(null),
sessionActivityId: yup.string().nullable(),
recurringTypeCode: yup
.string()
.nullable()
.when("$recurring", (recurring, schema) => { // Use context variable
const isRecurring = recurring[0] ?? false;
return isRecurring
? schema.oneOf(recurringTypeOptions.map((r) => r.code), "Invalid recurring type")
: schema.nullable();
}),
recurringStartDate: yup.string()
.nullable()
.when("$recurring", (recurring, schema) => {
const isRecurring = recurring[0] ?? false;
return isRecurring
? schema
.required("Recurring start date is required when recurring")
.matches(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/,
"Recurring start date must be a valid UTC ISO string (e.g., 'YYYY-MM-DDTHH:mm:ssZ')"
)
.test("is-future", "Recurring start date must be in the future", (value) => {
if (!value) return true; // Nullable when not required
return dayjs(value).isAfter(dayjs());
})
: schema.nullable();
}),
template: yup.object().shape({
id: yup.number().nullable().default(0),
mailingId: yup.number().default(0),
name: yup.string().default(""),
domainId: yup
.number()
.typeError("Domain is required")
.required("Domain is required")
.test("valid-domain", "Invalid domain", function (value) {
const setupData = this.options.context?.setupData as SetupData;
return setupData.emailDomains.some((d) => d.id === value);
}),
description: yup.string().default(""),
htmlBody: yup.string().default(""),
subject: yup.string().required("Subject is required").default(""),
toName: yup.string().default(""),
fromName: yup.string().required("From Name is required").default(""),
fromEmail: yup.string().default(""),
replyToEmail: yup.string().default(""),
clickTracking: yup.boolean().default(false),
openTracking: yup.boolean().default(false),
categoryXml: yup.string().default(""),
}),
target: yup.object().shape({
id: yup.number().nullable().default(0),
mailingId: yup.number().default(0),
serverId: yup.number().default(0),
name: yup.string().default(""),
databaseName: yup.string().default(""),
viewName: yup.string().default(""),
filterQuery: yup.string().default(""),
allowWriteBack: yup.boolean().default(false),
}).nullable(),
});
const { register, trigger, control, handleSubmit, reset, setValue, formState: { errors } } = useForm<Mailing>({ const { register, trigger, control, handleSubmit, reset, setValue, formState: { errors } } = useForm<Mailing>({
mode: "onBlur", mode: "onBlur",
defaultValues: { defaultValues: {
@ -251,6 +254,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
const initializeMailingEdit = async () => { const initializeMailingEdit = async () => {
if (open) { if (open) {
@ -314,7 +318,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
} }
try { try {
const jsonPayload = JSON.stringify(formData); const jsonPayload = JSON.stringify(formData);
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: jsonPayload, body: jsonPayload,
@ -357,7 +361,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
}; };
// Make the API call to /api/mailings/test // Make the API call to /api/mailings/test
const response = await fetch("/api/mailings/test", { const response = await customFetch("/api/mailings/test", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -387,7 +391,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
setTargetSampleLoading(true); setTargetSampleLoading(true);
try { try {
const response = await fetch(`/api/targets/${currentTarget.id}/sample`); const response = await customFetch(`/api/targets/${currentTarget.id}/sample`);
if (!response.ok) throw new Error("Failed to fetch sample data"); if (!response.ok) throw new Error("Failed to fetch sample data");
const data: TargetSample = await response.json(); const data: TargetSample = await response.json();
setTargetSample(data); setTargetSample(data);

View File

@ -28,6 +28,7 @@ import TemplateViewer from "@/components/modals/TemplateViewer";
import TargetSampleModal from "@/components/modals/TargetSampleModal"; import TargetSampleModal from "@/components/modals/TargetSampleModal";
import Target from "@/types/target"; import Target from "@/types/target";
import Template from "@/types/template"; import Template from "@/types/template";
import { useCustomFetch } from "@/utils/customFetch";
interface MailingViewProps { interface MailingViewProps {
open: boolean; open: boolean;
@ -48,6 +49,7 @@ interface MailingEmail {
} }
function MailingView({ open, mailing, onClose }: MailingViewProps) { function MailingView({ open, mailing, onClose }: MailingViewProps) {
const customFetch = useCustomFetch();
const setupData = useSetupData(); const setupData = useSetupData();
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false); const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
const [targetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false); const [targetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
@ -65,7 +67,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
} }
setLoading(true); setLoading(true);
const emailsResponse = await fetch(`/api/mailings/${mailing.id}/emails`); const emailsResponse = await customFetch(`/api/mailings/${mailing.id}/emails`);
const emailsData = await emailsResponse.json(); const emailsData = await emailsResponse.json();
if (emailsData) { if (emailsData) {

View File

@ -12,6 +12,7 @@ import Server from "@/types/server";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useCustomFetch } from "@/utils/customFetch";
type ServerEditProps = { type ServerEditProps = {
open: boolean; open: boolean;
@ -51,6 +52,7 @@ const defaultServer: Server = {
password: "", password: "",
}; };
const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => { const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
const customFetch = useCustomFetch();
const isNew = !server || server.id === 0; const isNew = !server || server.id === 0;
const originalServer: Server | null = server ? { ...server } : null; const originalServer: Server | null = server ? { ...server } : null;
//const [formData, setFormData] = useState<Server>({ ...server }); //const [formData, setFormData] = useState<Server>({ ...server });
@ -73,7 +75,7 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
const method = isNew ? "POST" : "PUT"; const method = isNew ? "POST" : "PUT";
setLoading(true); setLoading(true);
try { try {
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),

View File

@ -24,6 +24,7 @@ import TargetColumn from "@/types/targetColumn";
import TargetSampleColumn from "@/types/targetSampleColumn"; import TargetSampleColumn from "@/types/targetSampleColumn";
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid"; import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
import { GridColDef } from '@mui/x-data-grid'; import { GridColDef } from '@mui/x-data-grid';
import { useCustomFetch } from "@/utils/customFetch";
type TargetEditProps = { type TargetEditProps = {
open: boolean; open: boolean;
@ -96,6 +97,7 @@ const useColumnErrors = (errors: any, columns: TargetColumn[]) => {
}; };
const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => { const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
const customFetch = useCustomFetch();
const isNew = !target || target.id === 0; const isNew = !target || target.id === 0;
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const [targetTested, setTargetTested] = useState<boolean>(false); const [targetTested, setTargetTested] = useState<boolean>(false);
@ -136,7 +138,7 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
const method = isNew ? "POST" : "PUT"; const method = isNew ? "POST" : "PUT";
setLoading(true); setLoading(true);
try { try {
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),
@ -172,7 +174,7 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
const handleTestTarget = async () => { const handleTestTarget = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await fetch(`/api/targets/test`, { const response = await customFetch(`/api/targets/test`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({

View File

@ -13,6 +13,7 @@ import CloseIcon from '@mui/icons-material/Close';
import TargetSample from "@/types/targetSample"; import TargetSample from "@/types/targetSample";
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid"; import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
import { GridColDef } from '@mui/x-data-grid'; import { GridColDef } from '@mui/x-data-grid';
import { useCustomFetch } from "@/utils/customFetch";
type TargetSampleModalProps = { type TargetSampleModalProps = {
open: boolean; open: boolean;
@ -22,6 +23,7 @@ type TargetSampleModalProps = {
}; };
const TargetSampleModal = ({ open, target, targetSample, onClose }: TargetSampleModalProps) => { const TargetSampleModal = ({ open, target, targetSample, onClose }: TargetSampleModalProps) => {
const customFetch = useCustomFetch();
const [localTargetSample, setLocalTargetSample] = useState<TargetSample | null>(null); const [localTargetSample, setLocalTargetSample] = useState<TargetSample | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -34,7 +36,7 @@ const TargetSampleModal = ({ open, target, targetSample, onClose }: TargetSample
else if (target.id) { else if (target.id) {
setLoading(true); setLoading(true);
try { try {
const response = await fetch(`/api/targets/${target.id}/sample`); const response = await customFetch(`/api/targets/${target.id}/sample`);
if (!response.ok) throw new Error("Failed to fetch sample data"); if (!response.ok) throw new Error("Failed to fetch sample data");
const data: TargetSample = await response.json(); const data: TargetSample = await response.json();
setLocalTargetSample(data); setLocalTargetSample(data);

View File

@ -21,6 +21,7 @@ import Editor from "@monaco-editor/react";
// Assuming these types and context are defined elsewhere // Assuming these types and context are defined elsewhere
import Template from "@/types/template"; import Template from "@/types/template";
import { useSetupData } from "@/context/SetupDataContext"; import { useSetupData } from "@/context/SetupDataContext";
import { useCustomFetch } from "@/utils/customFetch";
type TemplateEditProps = { type TemplateEditProps = {
open: boolean; open: boolean;
@ -64,6 +65,7 @@ const defaultTemplate: Template = {
}; };
const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) => { const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) => {
const customFetch = useCustomFetch();
const isNew = !template || template.id === 0; const isNew = !template || template.id === 0;
const setupData = useSetupData(); const setupData = useSetupData();
@ -100,7 +102,7 @@ const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) =>
const method = isNew ? "POST" : "PUT"; const method = isNew ? "POST" : "PUT";
setLoading(true); setLoading(true);
try { try {
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method, method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),

View File

@ -13,6 +13,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import TestEmailList from "@/types/testEmailList"; import TestEmailList from "@/types/testEmailList";
import { useSetupData, SetupData } from "@/context/SetupDataContext"; import { useSetupData, SetupData } from "@/context/SetupDataContext";
import { useCustomFetch } from "@/utils/customFetch";
type TestEmailListEditProps = { type TestEmailListEditProps = {
open: boolean; open: boolean;
@ -56,6 +57,7 @@ const defaultTestEmailList: TestEmailListForm = {
}; };
const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailListEditProps) => { const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailListEditProps) => {
const customFetch = useCustomFetch();
const isNew = !testEmailList || testEmailList.id === 0; const isNew = !testEmailList || testEmailList.id === 0;
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -90,7 +92,7 @@ const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailLi
const method = isNew ? "POST" : "PUT"; const method = isNew ? "POST" : "PUT";
setLoading(true); setLoading(true);
try { try {
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method, method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(testEmailListDto), body: JSON.stringify(testEmailListDto),

View File

@ -11,6 +11,7 @@ import UnsubscribeUrl from "@/types/unsubscribeUrl";
import { useForm, Resolver } from "react-hook-form"; import { useForm, Resolver } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useCustomFetch } from "@/utils/customFetch";
type UnsubscribeUrlEditProps = { type UnsubscribeUrlEditProps = {
open: boolean; open: boolean;
@ -32,6 +33,7 @@ const defaultUnsubscribeUrl: UnsubscribeUrl = {
}; };
const UnsubscribeUrlEdit = ({ open, unsubscribeUrl, onClose, onSave }: UnsubscribeUrlEditProps) => { const UnsubscribeUrlEdit = ({ open, unsubscribeUrl, onClose, onSave }: UnsubscribeUrlEditProps) => {
const customFetch = useCustomFetch();
const isNew = !unsubscribeUrl || unsubscribeUrl.id === 0; const isNew = !unsubscribeUrl || unsubscribeUrl.id === 0;
const { register, handleSubmit, reset, formState: { errors } } = useForm<UnsubscribeUrl>({ const { register, handleSubmit, reset, formState: { errors } } = useForm<UnsubscribeUrl>({
@ -53,7 +55,7 @@ const UnsubscribeUrlEdit = ({ open, unsubscribeUrl, onClose, onSave }: Unsubscri
const method = isNew ? "POST" : "PUT"; const method = isNew ? "POST" : "PUT";
setLoading(true); setLoading(true);
try { try {
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),

View File

@ -1,12 +1,16 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext";
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton, FormControlLabel } from '@mui/material'; import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton, FormControlLabel } from '@mui/material';
import { DataGrid, GridColDef, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import MailingStatistic from '@/types/mailingStatistic'; // Assuming you'll create this type based on the C# class import MailingStatistic from '@/types/mailingStatistic';
import { useCustomFetch } from "@/utils/customFetch";
function ActiveMailings() { function ActiveMailings() {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const setupData: SetupData = useSetupData();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const gridContainerRef = useRef<HTMLDivElement | null>(null); const gridContainerRef = useRef<HTMLDivElement | null>(null);
@ -38,8 +42,9 @@ function ActiveMailings() {
isFetchingRef.current = true; isFetchingRef.current = true;
setMailingsLoading(true); setMailingsLoading(true);
setupData.reloadSetupData();
try { try {
const response = await fetch("/api/mailings/status/SD/stats"); const response = await customFetch("/api/mailings/status/SD/stats");
const statsData = await response.json(); const statsData = await response.json();
if (statsData) { if (statsData) {
setMailingStats(statsData); setMailingStats(statsData);

View File

@ -7,6 +7,25 @@ import { TitleProvider } from "@/context/TitleContext";
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import { getWebInstrumentations, initializeFaro } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
initializeFaro({
url: 'https://faro-collector-prod-us-west-0.grafana.net/collect/4be6b1cb063deee9f9f54fc46da6c6e7',
app: {
name: 'Surge365 Mass Email',
version: '1.0.0',
environment: 'production'
},
instrumentations: [
// Mandatory, omits default instrumentations otherwise.
...getWebInstrumentations(),
// Tracing package to get end-to-end visibility for HTTP requests.
new TracingInstrumentation(),
],
});
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
if (rootElement) { if (rootElement) {

View File

@ -7,8 +7,10 @@ import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Circ
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton, GridDeleteIcon } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton, GridDeleteIcon } from '@mui/x-data-grid';
import BouncedEmail from '@/types/bouncedEmail'; import BouncedEmail from '@/types/bouncedEmail';
import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit"; import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit";
import { useCustomFetch } from "@/utils/customFetch";
function BouncedEmails() { function BouncedEmails() {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
@ -54,7 +56,7 @@ function BouncedEmails() {
const method = "DELETE"; const method = "DELETE";
//setLoading(true); //setLoading(true);
try { try {
const response = await fetch(apiUrl, { const response = await customFetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
}); });

View File

@ -7,9 +7,11 @@ import { Box, useTheme, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import Mailing from '@/types/mailing'; import Mailing from '@/types/mailing';
import MailingEdit from "@/components/modals/MailingEdit"; import MailingEdit from "@/components/modals/MailingEdit";
import MailingView from "@/components/modals/MailingView"; // Assume this is a new read-only view component import MailingView from "@/components/modals/MailingView";
import { useCustomFetch } from "@/utils/customFetch";
function CancelledMailings() { function CancelledMailings() {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
@ -51,7 +53,8 @@ function CancelledMailings() {
const reloadMailings = async () => { const reloadMailings = async () => {
setMailingsLoading(true); setMailingsLoading(true);
const mailingsResponse = await fetch("/api/mailings/status/c"); setupData.reloadSetupData();
const mailingsResponse = await customFetch("/api/mailings/status/c");
const mailingsData = await mailingsResponse.json(); const mailingsData = await mailingsResponse.json();
if (mailingsData) { if (mailingsData) {
setMailings(mailingsData); setMailings(mailingsData);

View File

@ -9,8 +9,10 @@ import MailingStatistic from '@/types/mailingStatistic';
import Mailing from '@/types/mailing'; import Mailing from '@/types/mailing';
import MailingView from "@/components/modals/MailingView"; import MailingView from "@/components/modals/MailingView";
import MailingEdit from "@/components/modals/MailingEdit"; import MailingEdit from "@/components/modals/MailingEdit";
import { useCustomFetch } from "@/utils/customFetch";
function CompletedMailings() { function CompletedMailings() {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
@ -93,7 +95,7 @@ function CompletedMailings() {
if (params.length > 0) { if (params.length > 0) {
url += `?${params.join('&')}`; url += `?${params.join('&')}`;
} }
const response = await fetch(url); const response = await customFetch(url);
const statsData = await response.json(); const statsData = await response.json();
if (statsData) { if (statsData) {
setMailingStats(statsData); setMailingStats(statsData);
@ -109,7 +111,7 @@ function CompletedMailings() {
const fetchMailingDetails = async (mailingId: number) => { const fetchMailingDetails = async (mailingId: number) => {
try { try {
const response = await fetch(`/api/mailings/${mailingId}`); const response = await customFetch(`/api/mailings/${mailingId}`);
const mailingData = await response.json(); const mailingData = await response.json();
if (mailingData) { if (mailingData) {
return mailingData; return mailingData;

View File

@ -8,8 +8,10 @@ import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Butt
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import EmailDomain from '@/types/emailDomain'; import EmailDomain from '@/types/emailDomain';
import EmailDomainEdit from "@/components/modals/EmailDomainEdit"; import EmailDomainEdit from "@/components/modals/EmailDomainEdit";
import { useCustomFetch } from "@/utils/customFetch";
function EmailDomains() { function EmailDomains() {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
@ -69,7 +71,7 @@ function EmailDomains() {
} }
}; };
const loadEmailDomainsWithPasswords = async () => { const loadEmailDomainsWithPasswords = async () => {
const response = await fetch("/api/emailDomains/GetAll?activeOnly=false&returnPassword=true"); const response = await customFetch("/api/emailDomains/GetAll?activeOnly=false&returnPassword=true");
const data = await response.json(); const data = await response.json();
setEmailDomainsWithPasswords(data); setEmailDomainsWithPasswords(data);
} }

View File

@ -10,8 +10,10 @@ import Mailing from '@/types/mailing';
//import Template from '@/types/template'; //import Template from '@/types/template';
import MailingEdit from "@/components/modals/MailingEdit"; import MailingEdit from "@/components/modals/MailingEdit";
import ConfirmationDialog from "@/components/modals/ConfirmationDialog"; import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
import { useCustomFetch } from "@/utils/customFetch";
function NewMailings() { function NewMailings() {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
@ -55,8 +57,9 @@ function NewMailings() {
const reloadMailings = async () => { const reloadMailings = async () => {
setMailingsLoading(true); setMailingsLoading(true);
setupData.reloadSetupData();
const mailingsResponse = await fetch("/api/mailings/status/ed"); const mailingsResponse = await customFetch("/api/mailings/status/ed");
const mailingsData = await mailingsResponse.json(); const mailingsData = await mailingsResponse.json();
if (mailingsData) { if (mailingsData) {
setMailings(mailingsData); setMailings(mailingsData);
@ -94,7 +97,7 @@ function NewMailings() {
if (!mailingToCancel) return; if (!mailingToCancel) return;
try { try {
const response = await fetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' }); const response = await customFetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' });
if (response.ok) { if (response.ok) {
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id)); setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
} else { } else {

View File

@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext";
import VisibilityIcon from '@mui/icons-material/Visibility'; import VisibilityIcon from '@mui/icons-material/Visibility';
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@ -9,9 +10,12 @@ import Mailing from '@/types/mailing';
import MailingEdit from "@/components/modals/MailingEdit"; import MailingEdit from "@/components/modals/MailingEdit";
import MailingView from "@/components/modals/MailingView"; import MailingView from "@/components/modals/MailingView";
import ConfirmationDialog from "@/components/modals/ConfirmationDialog"; import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
import { useCustomFetch } from "@/utils/customFetch";
function ScheduleMailings() { function ScheduleMailings() {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const setupData: SetupData = useSetupData();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const gridContainerRef = useRef<HTMLDivElement | null>(null); const gridContainerRef = useRef<HTMLDivElement | null>(null);
@ -79,7 +83,8 @@ function ScheduleMailings() {
const reloadMailings = async () => { const reloadMailings = async () => {
setMailingsLoading(true); setMailingsLoading(true);
const mailingsResponse = await fetch("/api/mailings/status/sc"); // Adjust endpoint as needed setupData.reloadSetupData();
const mailingsResponse = await customFetch("/api/mailings/status/sc"); // Adjust endpoint as needed
const mailingsData = await mailingsResponse.json(); const mailingsData = await mailingsResponse.json();
if (mailingsData) { if (mailingsData) {
setMailings(mailingsData); setMailings(mailingsData);
@ -111,7 +116,7 @@ function ScheduleMailings() {
if (!mailingToCancel) return; if (!mailingToCancel) return;
try { try {
const response = await fetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' }); const response = await customFetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' });
if (response.ok) { if (response.ok) {
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id)); setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
} else { } else {

View File

@ -7,12 +7,14 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material'; import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import { Lock, LockOpen } from "@mui/icons-material"; import { Lock, LockOpen } from "@mui/icons-material";
//import utils from '@/ts/utils'; //import utils from '@/utils/utils';
import Server from '@/types/server'; import Server from '@/types/server';
import ServerEdit from "@/components/modals/ServerEdit"; import ServerEdit from "@/components/modals/ServerEdit";
import { useCustomFetch } from "@/utils/customFetch";
function Servers() { function Servers() {
const customFetch = useCustomFetch();
const theme = useTheme(); const theme = useTheme();
const setupData: SetupData = useSetupData(); const setupData: SetupData = useSetupData();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
@ -31,7 +33,7 @@ function Servers() {
else { else {
try { try {
setIsPasswordVisible(true); setIsPasswordVisible(true);
const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=true"); const serversResponse = await customFetch("/api/servers/GetAll?activeOnly=false&returnPassword=true");
const serversData = await serversResponse.json(); const serversData = await serversResponse.json();
setServersWithPasswords(serversData); setServersWithPasswords(serversData);
} }

View File

@ -5,11 +5,13 @@ import dayjs from 'dayjs'; // Import Dayjs for date handling
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone'; import timezone from 'dayjs/plugin/timezone';
import MailingStatistic from '@/types/mailingStatistic'; import MailingStatistic from '@/types/mailingStatistic';
import { useCustomFetch } from "@/utils/customFetch";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
export default function RecentMailingStatsChart({ days = 7 }: { days?: number }) { export default function RecentMailingStatsChart({ days = 7 }: { days?: number }) {
const customFetch = useCustomFetch();
const [stats, setStats] = useState<MailingStatistic[]>([]); const [stats, setStats] = useState<MailingStatistic[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -22,7 +24,7 @@ export default function RecentMailingStatsChart({ days = 7 }: { days?: number })
const fetchStats = async () => { const fetchStats = async () => {
try { try {
const response = await fetch(`/api/mailings/status/s%2Csd/stats?startDate=${startDateString}&endDate=${endDateString}`); const response = await customFetch(`/api/mailings/status/s%2Csd/stats?startDate=${startDateString}&endDate=${endDateString}`);
const data: MailingStatistic[] = await response.json(); const data: MailingStatistic[] = await response.json();
setStats(data); setStats(data);
} catch (error) { } catch (error) {

View File

@ -0,0 +1,55 @@
import { useAuth } from '@/components/auth/AuthContext';
import { useNavigate } from 'react-router-dom';
interface FetchOptions extends RequestInit {
headers?: Record<string, string>;
}
const customFetch = async (
url: string,
options: FetchOptions = {},
authContext: ReturnType<typeof useAuth>,
navigate: ReturnType<typeof useNavigate>
): Promise<Response> => {
const { accessToken, refreshToken } = authContext;
const headers = {
...options.headers,
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
};
const response = await fetch(url, {
...options,
headers,
});
if (response.status === 401) {
const refreshSuccess = await refreshToken();
if (refreshSuccess) {
const newAccessToken = authContext.accessToken;
const retryResponse = await fetch(url, {
...options,
headers: {
...options.headers,
...(newAccessToken ? { Authorization: `Bearer ${newAccessToken}` } : {}),
},
});
return retryResponse;
} else {
navigate('/login');
throw new Error('Authentication failed');
}
}
return response;
};
export const useCustomFetch = () => {
const authContext = useAuth();
const navigate = useNavigate();
return (url: string, options: FetchOptions = {}) =>
customFetch(url, options, authContext, navigate);
};
export default customFetch;

View File

@ -28,7 +28,7 @@ const utils = {
/*The following may not be needed any longer? /*The following may not be needed any longer?
**TODO: WebMethod should be changed to mimic fetch command but add in auth headers? **TODO: WebMethod should be changed to mimic fetch command but add in auth headers?
fetch('/api/protected-endpoint', { customFetch('/api/protected-endpoint', {
headers: { headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}` 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
} }
@ -98,7 +98,7 @@ const utils = {
// //const timeoutId = setTimeout(() => controller.abort(), timeout); // //const timeoutId = setTimeout(() => controller.abort(), timeout);
// setTimeout(() => controller.abort(), timeout); // setTimeout(() => controller.abort(), timeout);
// const response = await fetch(url, { // const response = await customFetch(url, {
// method: httpMethod, // method: httpMethod,
// headers, // headers,
// body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)), // body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)),