Compare commits

..

No commits in common. "7faac8b448ed736b87488f535e919bf3bbd59d77" and "3bd334f2398359858036c562def00745b9518da2" have entirely different histories.

38 changed files with 872 additions and 2915 deletions

View File

@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http;
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;
@ -8,7 +7,6 @@ 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;
@ -28,13 +26,6 @@ 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" });
} }
@ -57,7 +48,6 @@ 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 });
@ -84,7 +74,6 @@ 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,12 +1,10 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc;
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,122 +6,72 @@ 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;
try var keyVaultName = builder.Configuration["KeyVaultName"] ?? "";
if (!string.IsNullOrEmpty(keyVaultName))
{ {
var keyVaultName = builder.Configuration["KeyVaultName"] ?? ""; var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/";
if (!string.IsNullOrEmpty(keyVaultName)) builder.Configuration.AddAzureKeyVault(
{ new Uri(keyVaultUri),
var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/"; new DefaultAzureCredential(),
builder.Configuration.AddAzureKeyVault( new SurgeKeyVaultSecretManager()
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 =>
{ {
LoggingService appLoggingService = new LoggingService(builder.Configuration); client.BaseAddress = new Uri("https://api.sendgrid.com/"); // Optional, for clarity
appLoggingService.LogError(ex).Wait(); }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
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.Run(); app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
DapperConfiguration.ConfigureMappings();
app.Run();

View File

@ -10,7 +10,6 @@
</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

@ -1,21 +0,0 @@
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

@ -1,66 +0,0 @@
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,7 +67,6 @@ 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

@ -1,66 +0,0 @@
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,7 +14,6 @@
<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,8 +14,6 @@
"@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 '@/utils/utils'; import utils from '@/ts/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 '@/utils/utils'; import utils from '@/ts/utils';
import { Box, CircularProgress, Typography } from '@mui/material'; import { Box, CircularProgress, Typography } from '@mui/material';
interface AuthContextType { interface AuthContextType {
@ -7,7 +7,6 @@ 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>({
@ -15,7 +14,6 @@ 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 }) => {
@ -35,26 +33,6 @@ 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 () => {
@ -77,8 +55,9 @@ 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
} }
}; };
@ -86,7 +65,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}, []); }, []);
return ( return (
<AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading, refreshToken }}> <AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading }}>
{isLoading ? ( {isLoading ? (
<Box <Box
sx={{ sx={{

View File

@ -45,7 +45,6 @@ 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
@ -80,7 +79,6 @@ 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);
@ -120,7 +118,7 @@ const Layout = ({ children }: LayoutProps) => {
const handleRefreshUser = async () => { const handleRefreshUser = async () => {
handleCloseProfileMenu(); handleCloseProfileMenu();
try { try {
const response = await customFetch('/api/authentication/refreshtoken', { const response = await fetch('/api/authentication/refreshtoken', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
@ -140,7 +138,7 @@ const Layout = ({ children }: LayoutProps) => {
const handleLogout = async () => { const handleLogout = async () => {
setAuth(null); // Clear context setAuth(null); // Clear context
await customFetch('/api/authentication/logout', { method: 'POST', credentials: 'include' }); await fetch('/api/authentication/logout', { method: 'POST', credentials: 'include' });
navigate('/login'); navigate('/login');
}; };

View File

@ -14,7 +14,6 @@ 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;
@ -49,7 +48,6 @@ 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;
@ -75,7 +73,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 customFetch(apiUrl, { const response = await fetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),
@ -95,7 +93,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 Blocked Email" : "Edit Blocked Email"}</DialogTitle> <DialogTitle>{isNew ? "Add Bounced Email" : "Edit Bounced Email"}</DialogTitle>
<DialogContent> <DialogContent>
<TextField <TextField
{...register("emailAddress")} {...register("emailAddress")}

View File

@ -14,7 +14,6 @@ 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;
@ -57,7 +56,6 @@ 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;
@ -82,7 +80,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 customFetch(apiUrl, { const response = await fetch(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,8 +1,7 @@
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 '@/utils/utils'; import utils from '@/ts/utils';
import { useCustomFetch } from "@/utils/customFetch";
type FormErrors = Record<string, string>; type FormErrors = Record<string, string>;
@ -12,7 +11,6 @@ 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);
@ -38,7 +36,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 customFetch(apiUrl, { const response = await fetch(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,7 +40,6 @@ 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);
@ -67,8 +66,166 @@ 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);
@ -83,166 +240,6 @@ 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: {
@ -254,7 +251,6 @@ 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) {
@ -318,7 +314,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
} }
try { try {
const jsonPayload = JSON.stringify(formData); const jsonPayload = JSON.stringify(formData);
const response = await customFetch(apiUrl, { const response = await fetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: jsonPayload, body: jsonPayload,
@ -361,7 +357,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 customFetch("/api/mailings/test", { const response = await fetch("/api/mailings/test", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -391,7 +387,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
setTargetSampleLoading(true); setTargetSampleLoading(true);
try { try {
const response = await customFetch(`/api/targets/${currentTarget.id}/sample`); const response = await fetch(`/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,7 +28,6 @@ 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;
@ -49,7 +48,6 @@ 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);
@ -67,7 +65,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
} }
setLoading(true); setLoading(true);
const emailsResponse = await customFetch(`/api/mailings/${mailing.id}/emails`); const emailsResponse = await fetch(`/api/mailings/${mailing.id}/emails`);
const emailsData = await emailsResponse.json(); const emailsData = await emailsResponse.json();
if (emailsData) { if (emailsData) {

View File

@ -12,7 +12,6 @@ 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;
@ -52,7 +51,6 @@ 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 });
@ -75,7 +73,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 customFetch(apiUrl, { const response = await fetch(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,7 +24,6 @@ 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;
@ -97,7 +96,6 @@ 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);
@ -138,7 +136,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 customFetch(apiUrl, { const response = await fetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),
@ -174,7 +172,7 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
const handleTestTarget = async () => { const handleTestTarget = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await customFetch(`/api/targets/test`, { const response = await fetch(`/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,7 +13,6 @@ 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;
@ -23,7 +22,6 @@ 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);
@ -36,7 +34,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 customFetch(`/api/targets/${target.id}/sample`); const response = await fetch(`/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,7 +21,6 @@ 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;
@ -65,7 +64,6 @@ 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();
@ -102,7 +100,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 customFetch(apiUrl, { const response = await fetch(apiUrl, {
method, method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData), body: JSON.stringify(formData),

View File

@ -13,7 +13,6 @@ 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;
@ -57,7 +56,6 @@ 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);
@ -92,7 +90,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 customFetch(apiUrl, { const response = await fetch(apiUrl, {
method, method,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(testEmailListDto), body: JSON.stringify(testEmailListDto),

View File

@ -11,7 +11,6 @@ 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;
@ -33,7 +32,6 @@ 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>({
@ -55,7 +53,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 customFetch(apiUrl, { const response = await fetch(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,16 +1,12 @@
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'; import MailingStatistic from '@/types/mailingStatistic'; // Assuming you'll create this type based on the C# class
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);
@ -42,9 +38,8 @@ function ActiveMailings() {
isFetchingRef.current = true; isFetchingRef.current = true;
setMailingsLoading(true); setMailingsLoading(true);
setupData.reloadSetupData();
try { try {
const response = await customFetch("/api/mailings/status/SD/stats"); const response = await fetch("/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,25 +7,6 @@ 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,10 +7,8 @@ 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"));
@ -56,7 +54,7 @@ function BouncedEmails() {
const method = "DELETE"; const method = "DELETE";
//setLoading(true); //setLoading(true);
try { try {
const response = await customFetch(apiUrl, { const response = await fetch(apiUrl, {
method: method, method: method,
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
}); });

View File

@ -7,11 +7,9 @@ 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"; import MailingView from "@/components/modals/MailingView"; // Assume this is a new read-only view component
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();
@ -53,8 +51,7 @@ function CancelledMailings() {
const reloadMailings = async () => { const reloadMailings = async () => {
setMailingsLoading(true); setMailingsLoading(true);
setupData.reloadSetupData(); const mailingsResponse = await fetch("/api/mailings/status/c");
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,10 +9,8 @@ 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"));
@ -95,7 +93,7 @@ function CompletedMailings() {
if (params.length > 0) { if (params.length > 0) {
url += `?${params.join('&')}`; url += `?${params.join('&')}`;
} }
const response = await customFetch(url); const response = await fetch(url);
const statsData = await response.json(); const statsData = await response.json();
if (statsData) { if (statsData) {
setMailingStats(statsData); setMailingStats(statsData);
@ -111,7 +109,7 @@ function CompletedMailings() {
const fetchMailingDetails = async (mailingId: number) => { const fetchMailingDetails = async (mailingId: number) => {
try { try {
const response = await customFetch(`/api/mailings/${mailingId}`); const response = await fetch(`/api/mailings/${mailingId}`);
const mailingData = await response.json(); const mailingData = await response.json();
if (mailingData) { if (mailingData) {
return mailingData; return mailingData;

View File

@ -8,10 +8,8 @@ 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"));
@ -71,7 +69,7 @@ function EmailDomains() {
} }
}; };
const loadEmailDomainsWithPasswords = async () => { const loadEmailDomainsWithPasswords = async () => {
const response = await customFetch("/api/emailDomains/GetAll?activeOnly=false&returnPassword=true"); const response = await fetch("/api/emailDomains/GetAll?activeOnly=false&returnPassword=true");
const data = await response.json(); const data = await response.json();
setEmailDomainsWithPasswords(data); setEmailDomainsWithPasswords(data);
} }

View File

@ -10,10 +10,8 @@ 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"));
@ -57,9 +55,8 @@ function NewMailings() {
const reloadMailings = async () => { const reloadMailings = async () => {
setMailingsLoading(true); setMailingsLoading(true);
setupData.reloadSetupData();
const mailingsResponse = await customFetch("/api/mailings/status/ed"); const mailingsResponse = await fetch("/api/mailings/status/ed");
const mailingsData = await mailingsResponse.json(); const mailingsData = await mailingsResponse.json();
if (mailingsData) { if (mailingsData) {
setMailings(mailingsData); setMailings(mailingsData);
@ -97,7 +94,7 @@ function NewMailings() {
if (!mailingToCancel) return; if (!mailingToCancel) return;
try { try {
const response = await customFetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' }); const response = await fetch(`/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,5 +1,4 @@
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';
@ -10,12 +9,9 @@ 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);
@ -83,8 +79,7 @@ function ScheduleMailings() {
const reloadMailings = async () => { const reloadMailings = async () => {
setMailingsLoading(true); setMailingsLoading(true);
setupData.reloadSetupData(); const mailingsResponse = await fetch("/api/mailings/status/sc"); // Adjust endpoint as needed
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);
@ -116,7 +111,7 @@ function ScheduleMailings() {
if (!mailingToCancel) return; if (!mailingToCancel) return;
try { try {
const response = await customFetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' }); const response = await fetch(`/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,14 +7,12 @@ 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 '@/utils/utils'; //import utils from '@/ts/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"));
@ -33,7 +31,7 @@ function Servers() {
else { else {
try { try {
setIsPasswordVisible(true); setIsPasswordVisible(true);
const serversResponse = await customFetch("/api/servers/GetAll?activeOnly=false&returnPassword=true"); const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=true");
const serversData = await serversResponse.json(); const serversData = await serversResponse.json();
setServersWithPasswords(serversData); setServersWithPasswords(serversData);
} }

View File

@ -5,13 +5,11 @@ 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);
@ -24,7 +22,7 @@ export default function RecentMailingStatsChart({ days = 7 }: { days?: number })
const fetchStats = async () => { const fetchStats = async () => {
try { try {
const response = await customFetch(`/api/mailings/status/s%2Csd/stats?startDate=${startDateString}&endDate=${endDateString}`); const response = await fetch(`/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

@ -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?
customFetch('/api/protected-endpoint', { fetch('/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 customFetch(url, { // const response = await fetch(url, {
// method: httpMethod, // method: httpMethod,
// headers, // headers,
// body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)), // body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)),

View File

@ -1,55 +0,0 @@
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;