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.
This commit is contained in:
parent
3bd334f239
commit
0e099bfd07
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Surge365.MassEmailReact.API.Controllers;
|
||||
using Surge365.MassEmailReact.Application.DTOs;
|
||||
@ -7,6 +8,7 @@ using Surge365.MassEmailReact.Domain.Entities;
|
||||
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class AuthenticationController : BaseController
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
@ -26,6 +28,13 @@ namespace Surge365.MassEmailReact.API.Controllers
|
||||
SameSite = SameSiteMode.Strict,
|
||||
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" });
|
||||
}
|
||||
@ -48,6 +57,7 @@ namespace Surge365.MassEmailReact.API.Controllers
|
||||
Expires = DateTimeOffset.UtcNow.AddDays(7)
|
||||
};
|
||||
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
|
||||
Response.Cookies.Append("accessToken", authResponse.data.Value.accessToken, cookieOptions);
|
||||
|
||||
//TODO: Store user in session
|
||||
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)
|
||||
};
|
||||
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 });
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Surge365.MassEmailReact.API.Controllers
|
||||
{
|
||||
[Route("[controller]")]
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class BaseController : ControllerBase
|
||||
{
|
||||
}
|
||||
|
||||
@ -6,72 +6,122 @@ using Surge365.MassEmailReact.Infrastructure;
|
||||
using Surge365.MassEmailReact.Infrastructure.DapperMaps;
|
||||
using Surge365.MassEmailReact.Infrastructure.Repositories;
|
||||
using Surge365.MassEmailReact.Infrastructure.Services;
|
||||
using Surge365.MassEmailReact.Infrastructure.Middleware;
|
||||
using System.Net;
|
||||
using System.Security.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var keyVaultName = builder.Configuration["KeyVaultName"] ?? "";
|
||||
if (!string.IsNullOrEmpty(keyVaultName))
|
||||
WebApplication? app = null;
|
||||
try
|
||||
{
|
||||
var keyVaultName = builder.Configuration["KeyVaultName"] ?? "";
|
||||
if (!string.IsNullOrEmpty(keyVaultName))
|
||||
{
|
||||
var keyVaultUri = $"https://{keyVaultName}.vault.azure.net/";
|
||||
builder.Configuration.AddAzureKeyVault(
|
||||
new Uri(keyVaultUri),
|
||||
new DefaultAzureCredential(),
|
||||
new SurgeKeyVaultSecretManager()
|
||||
);
|
||||
}
|
||||
}
|
||||
builder.Services.AddSingleton<ILoggingService, LoggingService>();
|
||||
|
||||
builder.Services.AddHttpClient("SendGridClient", client =>
|
||||
{
|
||||
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
|
||||
{
|
||||
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
{
|
||||
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
|
||||
});
|
||||
// Add services to the container.
|
||||
});
|
||||
// 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>();
|
||||
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 = builder.Build();
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.MapStaticAssets();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
|
||||
DapperConfiguration.ConfigureMappings();
|
||||
|
||||
app.Run();
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggingService appLoggingService = new LoggingService(builder.Configuration);
|
||||
appLoggingService.LogError(ex).Wait();
|
||||
return;
|
||||
}
|
||||
if (app != null)
|
||||
app.Run();
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
||||
<Version>9.*-*</Version>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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, "", "", "", "", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.4.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.Extensions.Configuration" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.2191419">
|
||||
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.2191419">
|
||||
<PropertyGroup>
|
||||
<StartupCommand>npm run dev</StartupCommand>
|
||||
<JavaScriptTestRoot>src\</JavaScriptTestRoot>
|
||||
|
||||
2897
Surge365.MassEmailReact.Web/package-lock.json
generated
2897
Surge365.MassEmailReact.Web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,8 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@fontsource/roboto": "^5.1.1",
|
||||
"@grafana/faro-web-sdk": "^1.18.0",
|
||||
"@grafana/faro-web-tracing": "^1.18.0",
|
||||
"@hookform/resolvers": "^4.1.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^6.4.5",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import utils from '@/ts/utils';
|
||||
import utils from '@/utils/utils';
|
||||
import { useAuth } from '@/components/auth/AuthContext';
|
||||
|
||||
const AuthCheck: React.FC = () => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
interface AuthContextType {
|
||||
@ -7,6 +7,7 @@ interface AuthContextType {
|
||||
userRoles: string[];
|
||||
setAuth: (token: string | null) => void;
|
||||
isLoading: boolean; // Add loading state
|
||||
refreshToken: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
@ -14,6 +15,7 @@ const AuthContext = createContext<AuthContextType>({
|
||||
userRoles: [],
|
||||
setAuth: () => { },
|
||||
isLoading: true, // Default to loading
|
||||
refreshToken: async () => false,
|
||||
});
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
const initializeAuth = async () => {
|
||||
@ -55,9 +77,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
}
|
||||
} catch {
|
||||
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 (
|
||||
<AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading }}>
|
||||
<AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading, refreshToken }}>
|
||||
{isLoading ? (
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@ -45,6 +45,7 @@ import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
|
||||
// Constants
|
||||
@ -79,6 +80,7 @@ interface LayoutProps {
|
||||
}
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); //TODO: Move this to shared utils?
|
||||
const [open, setOpen] = React.useState(!isMobile);
|
||||
@ -118,7 +120,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
const handleRefreshUser = async () => {
|
||||
handleCloseProfileMenu();
|
||||
try {
|
||||
const response = await fetch('/api/authentication/refreshtoken', {
|
||||
const response = await customFetch('/api/authentication/refreshtoken', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
|
||||
@ -138,7 +140,7 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
|
||||
const handleLogout = async () => {
|
||||
setAuth(null); // Clear context
|
||||
await fetch('/api/authentication/logout', { method: 'POST', credentials: 'include' });
|
||||
await customFetch('/api/authentication/logout', { method: 'POST', credentials: 'include' });
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import { useForm, Resolver, Controller } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
type BouncedEmailEditProps = {
|
||||
open: boolean;
|
||||
@ -48,6 +49,7 @@ const defaultBouncedEmail: BouncedEmail = {
|
||||
};
|
||||
|
||||
const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailEditProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const isNew = bouncedEmail == null;
|
||||
const setupData: SetupData = useSetupData();
|
||||
const originalBouncedEmail: BouncedEmail | null = bouncedEmail ? { ...bouncedEmail } : null;
|
||||
@ -73,7 +75,7 @@ const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailE
|
||||
const method = isNew ? "POST" : "PUT"
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
|
||||
@ -14,6 +14,7 @@ import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import { useForm, Controller, Resolver } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
type EmailDomainEditProps = {
|
||||
open: boolean;
|
||||
@ -56,6 +57,7 @@ const defaultEmailDomain: EmailDomain = {
|
||||
};
|
||||
|
||||
const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEditProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const isNew = !emailDomain || emailDomain.id === 0;
|
||||
const setupData: SetupData = useSetupData();
|
||||
const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null;
|
||||
@ -80,7 +82,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/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>;
|
||||
|
||||
@ -11,6 +12,7 @@ type ForgotPasswordModalProps = {
|
||||
};
|
||||
|
||||
const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose }) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const [username, setUsername] = useState('');
|
||||
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
||||
const [usernameNotFound, setUsernameNotFound] = useState(false);
|
||||
@ -36,7 +38,7 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
|
||||
if (validate()) {
|
||||
console.log('Processing forgot password for', username);
|
||||
const apiUrl = "/api/authentication/generatepasswordrecovery";
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username}),
|
||||
|
||||
@ -40,6 +40,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling
|
||||
import utc from 'dayjs/plugin/utc'; // Import the UTC plugin
|
||||
import { toast } from "react-toastify";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@ -174,12 +175,12 @@ const schema = yup.object().shape({
|
||||
});
|
||||
|
||||
const nameIsAvailable = async (id: number, name: string) => {
|
||||
const response = await fetch(`/api/mailings/available?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
|
||||
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 fetch(`/api/mailings/nextavailablename?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
|
||||
const response = await customFetch(`/api/mailings/nextavailablename?${id > 0 ? "id=" + id + "&" : ""}name=${name}`);
|
||||
const data = await response.json();
|
||||
return data.name;
|
||||
};
|
||||
@ -226,6 +227,7 @@ const defaultMailing: Mailing = {
|
||||
};
|
||||
|
||||
const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const isNew = !mailing || mailing.id === 0;
|
||||
const setupData: SetupData = useSetupData();
|
||||
const [approved, setApproved] = useState<boolean>(false);
|
||||
@ -314,7 +316,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
}
|
||||
try {
|
||||
const jsonPayload = JSON.stringify(formData);
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: jsonPayload,
|
||||
@ -357,7 +359,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
};
|
||||
|
||||
// Make the API call to /api/mailings/test
|
||||
const response = await fetch("/api/mailings/test", {
|
||||
const response = await customFetch("/api/mailings/test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -387,7 +389,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
||||
|
||||
setTargetSampleLoading(true);
|
||||
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");
|
||||
const data: TargetSample = await response.json();
|
||||
setTargetSample(data);
|
||||
|
||||
@ -28,6 +28,7 @@ import TemplateViewer from "@/components/modals/TemplateViewer";
|
||||
import TargetSampleModal from "@/components/modals/TargetSampleModal";
|
||||
import Target from "@/types/target";
|
||||
import Template from "@/types/template";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
interface MailingViewProps {
|
||||
open: boolean;
|
||||
@ -48,6 +49,7 @@ interface MailingEmail {
|
||||
}
|
||||
|
||||
function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
||||
const customFetch = useCustomFetch();
|
||||
const setupData = useSetupData();
|
||||
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
|
||||
const [targetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
||||
@ -65,7 +67,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
||||
}
|
||||
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();
|
||||
|
||||
if (emailsData) {
|
||||
|
||||
@ -12,6 +12,7 @@ import Server from "@/types/server";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
type ServerEditProps = {
|
||||
open: boolean;
|
||||
@ -51,6 +52,7 @@ const defaultServer: Server = {
|
||||
password: "",
|
||||
};
|
||||
const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const isNew = !server || server.id === 0;
|
||||
const originalServer: Server | null = server ? { ...server } : null;
|
||||
//const [formData, setFormData] = useState<Server>({ ...server });
|
||||
@ -73,7 +75,7 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
|
||||
@ -24,6 +24,7 @@ import TargetColumn from "@/types/targetColumn";
|
||||
import TargetSampleColumn from "@/types/targetSampleColumn";
|
||||
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
|
||||
import { GridColDef } from '@mui/x-data-grid';
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
type TargetEditProps = {
|
||||
open: boolean;
|
||||
@ -96,6 +97,7 @@ const useColumnErrors = (errors: any, columns: TargetColumn[]) => {
|
||||
};
|
||||
|
||||
const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const isNew = !target || target.id === 0;
|
||||
const setupData: SetupData = useSetupData();
|
||||
const [targetTested, setTargetTested] = useState<boolean>(false);
|
||||
@ -136,7 +138,7 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
@ -172,7 +174,7 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||
const handleTestTarget = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/targets/test`, {
|
||||
const response = await customFetch(`/api/targets/test`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
@ -13,6 +13,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import TargetSample from "@/types/targetSample";
|
||||
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
|
||||
import { GridColDef } from '@mui/x-data-grid';
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
type TargetSampleModalProps = {
|
||||
open: boolean;
|
||||
@ -22,6 +23,7 @@ type TargetSampleModalProps = {
|
||||
};
|
||||
|
||||
const TargetSampleModal = ({ open, target, targetSample, onClose }: TargetSampleModalProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const [localTargetSample, setLocalTargetSample] = useState<TargetSample | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@ -34,7 +36,7 @@ const TargetSampleModal = ({ open, target, targetSample, onClose }: TargetSample
|
||||
else if (target.id) {
|
||||
setLoading(true);
|
||||
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");
|
||||
const data: TargetSample = await response.json();
|
||||
setLocalTargetSample(data);
|
||||
|
||||
@ -21,6 +21,7 @@ import Editor from "@monaco-editor/react";
|
||||
// Assuming these types and context are defined elsewhere
|
||||
import Template from "@/types/template";
|
||||
import { useSetupData } from "@/context/SetupDataContext";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
type TemplateEditProps = {
|
||||
open: boolean;
|
||||
@ -64,6 +65,7 @@ const defaultTemplate: Template = {
|
||||
};
|
||||
|
||||
const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const isNew = !template || template.id === 0;
|
||||
const setupData = useSetupData();
|
||||
|
||||
@ -100,7 +102,7 @@ const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) =>
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
|
||||
@ -13,6 +13,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import TestEmailList from "@/types/testEmailList";
|
||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
type TestEmailListEditProps = {
|
||||
open: boolean;
|
||||
@ -56,6 +57,7 @@ const defaultTestEmailList: TestEmailListForm = {
|
||||
};
|
||||
|
||||
const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailListEditProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const isNew = !testEmailList || testEmailList.id === 0;
|
||||
const setupData: SetupData = useSetupData();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -90,7 +92,7 @@ const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailLi
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(testEmailListDto),
|
||||
|
||||
@ -11,6 +11,7 @@ import UnsubscribeUrl from "@/types/unsubscribeUrl";
|
||||
import { useForm, Resolver } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
type UnsubscribeUrlEditProps = {
|
||||
open: boolean;
|
||||
@ -32,6 +33,7 @@ const defaultUnsubscribeUrl: UnsubscribeUrl = {
|
||||
};
|
||||
|
||||
const UnsubscribeUrlEdit = ({ open, unsubscribeUrl, onClose, onSave }: UnsubscribeUrlEditProps) => {
|
||||
const customFetch = useCustomFetch();
|
||||
const isNew = !unsubscribeUrl || unsubscribeUrl.id === 0;
|
||||
|
||||
const { register, handleSubmit, reset, formState: { errors } } = useForm<UnsubscribeUrl>({
|
||||
@ -53,7 +55,7 @@ const UnsubscribeUrlEdit = ({ open, unsubscribeUrl, onClose, onSave }: Unsubscri
|
||||
const method = isNew ? "POST" : "PUT";
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
|
||||
@ -3,9 +3,11 @@ import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import Switch from '@mui/material/Switch';
|
||||
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 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() {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
@ -39,7 +41,7 @@ function ActiveMailings() {
|
||||
isFetchingRef.current = true;
|
||||
setMailingsLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/mailings/status/SD/stats");
|
||||
const response = await customFetch("/api/mailings/status/SD/stats");
|
||||
const statsData = await response.json();
|
||||
if (statsData) {
|
||||
setMailingStats(statsData);
|
||||
|
||||
@ -7,6 +7,25 @@ import { TitleProvider } from "@/context/TitleContext";
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
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');
|
||||
if (rootElement) {
|
||||
|
||||
@ -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 BouncedEmail from '@/types/bouncedEmail';
|
||||
import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
function BouncedEmails() {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const setupData: SetupData = useSetupData();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
@ -54,7 +56,7 @@ function BouncedEmails() {
|
||||
const method = "DELETE";
|
||||
//setLoading(true);
|
||||
try {
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await customFetch(apiUrl, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
|
||||
@ -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 Mailing from '@/types/mailing';
|
||||
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() {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const setupData: SetupData = useSetupData();
|
||||
|
||||
@ -51,7 +53,7 @@ function CancelledMailings() {
|
||||
|
||||
const reloadMailings = async () => {
|
||||
setMailingsLoading(true);
|
||||
const mailingsResponse = await fetch("/api/mailings/status/c");
|
||||
const mailingsResponse = await customFetch("/api/mailings/status/c");
|
||||
const mailingsData = await mailingsResponse.json();
|
||||
if (mailingsData) {
|
||||
setMailings(mailingsData);
|
||||
|
||||
@ -9,8 +9,10 @@ import MailingStatistic from '@/types/mailingStatistic';
|
||||
import Mailing from '@/types/mailing';
|
||||
import MailingView from "@/components/modals/MailingView";
|
||||
import MailingEdit from "@/components/modals/MailingEdit";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
function CompletedMailings() {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
@ -93,7 +95,7 @@ function CompletedMailings() {
|
||||
if (params.length > 0) {
|
||||
url += `?${params.join('&')}`;
|
||||
}
|
||||
const response = await fetch(url);
|
||||
const response = await customFetch(url);
|
||||
const statsData = await response.json();
|
||||
if (statsData) {
|
||||
setMailingStats(statsData);
|
||||
@ -109,7 +111,7 @@ function CompletedMailings() {
|
||||
|
||||
const fetchMailingDetails = async (mailingId: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/mailings/${mailingId}`);
|
||||
const response = await customFetch(`/api/mailings/${mailingId}`);
|
||||
const mailingData = await response.json();
|
||||
if (mailingData) {
|
||||
return mailingData;
|
||||
|
||||
@ -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 EmailDomain from '@/types/emailDomain';
|
||||
import EmailDomainEdit from "@/components/modals/EmailDomainEdit";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
function EmailDomains() {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const setupData: SetupData = useSetupData();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
@ -69,7 +71,7 @@ function EmailDomains() {
|
||||
}
|
||||
};
|
||||
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();
|
||||
setEmailDomainsWithPasswords(data);
|
||||
}
|
||||
|
||||
@ -10,8 +10,10 @@ import Mailing from '@/types/mailing';
|
||||
//import Template from '@/types/template';
|
||||
import MailingEdit from "@/components/modals/MailingEdit";
|
||||
import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
function NewMailings() {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const setupData: SetupData = useSetupData();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
@ -56,7 +58,7 @@ function NewMailings() {
|
||||
const reloadMailings = async () => {
|
||||
setMailingsLoading(true);
|
||||
|
||||
const mailingsResponse = await fetch("/api/mailings/status/ed");
|
||||
const mailingsResponse = await customFetch("/api/mailings/status/ed");
|
||||
const mailingsData = await mailingsResponse.json();
|
||||
if (mailingsData) {
|
||||
setMailings(mailingsData);
|
||||
@ -94,7 +96,7 @@ function NewMailings() {
|
||||
if (!mailingToCancel) return;
|
||||
|
||||
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) {
|
||||
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
|
||||
} else {
|
||||
|
||||
@ -9,8 +9,10 @@ import Mailing from '@/types/mailing';
|
||||
import MailingEdit from "@/components/modals/MailingEdit";
|
||||
import MailingView from "@/components/modals/MailingView";
|
||||
import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
function ScheduleMailings() {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
@ -79,7 +81,7 @@ function ScheduleMailings() {
|
||||
|
||||
const reloadMailings = async () => {
|
||||
setMailingsLoading(true);
|
||||
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();
|
||||
if (mailingsData) {
|
||||
setMailings(mailingsData);
|
||||
@ -111,7 +113,7 @@ function ScheduleMailings() {
|
||||
if (!mailingToCancel) return;
|
||||
|
||||
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) {
|
||||
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
|
||||
} else {
|
||||
|
||||
@ -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 { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||
import { Lock, LockOpen } from "@mui/icons-material";
|
||||
//import utils from '@/ts/utils';
|
||||
//import utils from '@/utils/utils';
|
||||
import Server from '@/types/server';
|
||||
import ServerEdit from "@/components/modals/ServerEdit";
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
|
||||
function Servers() {
|
||||
const customFetch = useCustomFetch();
|
||||
const theme = useTheme();
|
||||
const setupData: SetupData = useSetupData();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
@ -31,7 +33,7 @@ function Servers() {
|
||||
else {
|
||||
try {
|
||||
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();
|
||||
setServersWithPasswords(serversData);
|
||||
}
|
||||
|
||||
@ -5,11 +5,13 @@ import dayjs from 'dayjs'; // Import Dayjs for date handling
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import MailingStatistic from '@/types/mailingStatistic';
|
||||
import { useCustomFetch } from "@/utils/customFetch";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export default function RecentMailingStatsChart({ days = 7 }: { days?: number }) {
|
||||
const customFetch = useCustomFetch();
|
||||
const [stats, setStats] = useState<MailingStatistic[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@ -22,7 +24,7 @@ export default function RecentMailingStatsChart({ days = 7 }: { days?: number })
|
||||
|
||||
const fetchStats = async () => {
|
||||
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();
|
||||
setStats(data);
|
||||
} catch (error) {
|
||||
|
||||
55
Surge365.MassEmailReact.Web/src/utils/customFetch.ts
Normal file
55
Surge365.MassEmailReact.Web/src/utils/customFetch.ts
Normal 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;
|
||||
@ -28,7 +28,7 @@ const utils = {
|
||||
|
||||
/*The following may not be needed any longer?
|
||||
**TODO: WebMethod should be changed to mimic fetch command but add in auth headers?
|
||||
fetch('/api/protected-endpoint', {
|
||||
customFetch('/api/protected-endpoint', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
|
||||
}
|
||||
@ -98,7 +98,7 @@ const utils = {
|
||||
// //const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
// setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// const response = await fetch(url, {
|
||||
// const response = await customFetch(url, {
|
||||
// method: httpMethod,
|
||||
// headers,
|
||||
// body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)),
|
||||
Loading…
x
Reference in New Issue
Block a user