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 Microsoft.AspNetCore.Mvc;
|
||||||
using Surge365.MassEmailReact.API.Controllers;
|
using Surge365.MassEmailReact.API.Controllers;
|
||||||
using Surge365.MassEmailReact.Application.DTOs;
|
using Surge365.MassEmailReact.Application.DTOs;
|
||||||
@ -7,6 +8,7 @@ using Surge365.MassEmailReact.Domain.Entities;
|
|||||||
|
|
||||||
namespace Surge365.MassEmailReact.API.Controllers
|
namespace Surge365.MassEmailReact.API.Controllers
|
||||||
{
|
{
|
||||||
|
[AllowAnonymous]
|
||||||
public class AuthenticationController : BaseController
|
public class AuthenticationController : BaseController
|
||||||
{
|
{
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
@ -26,6 +28,13 @@ namespace Surge365.MassEmailReact.API.Controllers
|
|||||||
SameSite = SameSiteMode.Strict,
|
SameSite = SameSiteMode.Strict,
|
||||||
Expires = DateTimeOffset.UtcNow.AddDays(-1) // Expire immediately
|
Expires = DateTimeOffset.UtcNow.AddDays(-1) // Expire immediately
|
||||||
});
|
});
|
||||||
|
Response.Cookies.Append("accessToken", "", new CookieOptions
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
Secure = true,
|
||||||
|
SameSite = SameSiteMode.Strict,
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddDays(-1) // Expire immediately
|
||||||
|
});
|
||||||
|
|
||||||
return Ok(new { message = "Logged out successfully" });
|
return Ok(new { message = "Logged out successfully" });
|
||||||
}
|
}
|
||||||
@ -48,6 +57,7 @@ namespace Surge365.MassEmailReact.API.Controllers
|
|||||||
Expires = DateTimeOffset.UtcNow.AddDays(7)
|
Expires = DateTimeOffset.UtcNow.AddDays(7)
|
||||||
};
|
};
|
||||||
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
|
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
|
||||||
|
Response.Cookies.Append("accessToken", authResponse.data.Value.accessToken, cookieOptions);
|
||||||
|
|
||||||
//TODO: Store user in session
|
//TODO: Store user in session
|
||||||
return Ok(new { success = true, authResponse.data.Value.accessToken, authResponse.data.Value.user });
|
return Ok(new { success = true, authResponse.data.Value.accessToken, authResponse.data.Value.user });
|
||||||
@ -74,6 +84,7 @@ namespace Surge365.MassEmailReact.API.Controllers
|
|||||||
Expires = DateTimeOffset.UtcNow.AddDays(7)
|
Expires = DateTimeOffset.UtcNow.AddDays(7)
|
||||||
};
|
};
|
||||||
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
|
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
|
||||||
|
Response.Cookies.Append("accessToken", authResponse.data.Value.accessToken, cookieOptions);
|
||||||
|
|
||||||
return Ok(new { accessToken = authResponse.data.Value.accessToken });
|
return Ok(new { accessToken = authResponse.data.Value.accessToken });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Surge365.MassEmailReact.API.Controllers
|
namespace Surge365.MassEmailReact.API.Controllers
|
||||||
{
|
{
|
||||||
[Route("[controller]")]
|
[Route("[controller]")]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize]
|
||||||
public class BaseController : ControllerBase
|
public class BaseController : ControllerBase
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,13 +6,19 @@ 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"] ?? "";
|
var keyVaultName = builder.Configuration["KeyVaultName"] ?? "";
|
||||||
if (!string.IsNullOrEmpty(keyVaultName))
|
if (!string.IsNullOrEmpty(keyVaultName))
|
||||||
{
|
{
|
||||||
@ -23,6 +29,41 @@ if (!string.IsNullOrEmpty(keyVaultName))
|
|||||||
new SurgeKeyVaultSecretManager()
|
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 =>
|
builder.Services.AddHttpClient("SendGridClient", client =>
|
||||||
{
|
{
|
||||||
@ -54,8 +95,10 @@ builder.Services.AddScoped<IEmailDomainRepository, EmailDomainRepository>();
|
|||||||
builder.Services.AddScoped<IMailingService, MailingService>();
|
builder.Services.AddScoped<IMailingService, MailingService>();
|
||||||
builder.Services.AddScoped<IMailingRepository, MailingRepository>();
|
builder.Services.AddScoped<IMailingRepository, MailingRepository>();
|
||||||
|
|
||||||
var app = builder.Build();
|
app = builder.Build();
|
||||||
|
|
||||||
|
|
||||||
|
app.UseCustomExceptionHandler();
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
|
|
||||||
@ -73,5 +116,12 @@ app.MapControllers();
|
|||||||
|
|
||||||
|
|
||||||
DapperConfiguration.ConfigureMappings();
|
DapperConfiguration.ConfigureMappings();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggingService appLoggingService = new LoggingService(builder.Configuration);
|
||||||
|
appLoggingService.LogError(ex).Wait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (app != null)
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
||||||
<Version>9.*-*</Version>
|
<Version>9.*-*</Version>
|
||||||
|
|||||||
@ -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>
|
<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" />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
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/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",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import utils from '@/ts/utils';
|
import utils from '@/utils/utils';
|
||||||
import { useAuth } from '@/components/auth/AuthContext';
|
import { useAuth } from '@/components/auth/AuthContext';
|
||||||
|
|
||||||
const AuthCheck: React.FC = () => {
|
const AuthCheck: React.FC = () => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
import utils from '@/ts/utils';
|
import utils from '@/utils/utils';
|
||||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
@ -7,6 +7,7 @@ interface AuthContextType {
|
|||||||
userRoles: string[];
|
userRoles: string[];
|
||||||
setAuth: (token: string | null) => void;
|
setAuth: (token: string | null) => void;
|
||||||
isLoading: boolean; // Add loading state
|
isLoading: boolean; // Add loading state
|
||||||
|
refreshToken: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType>({
|
const AuthContext = createContext<AuthContextType>({
|
||||||
@ -14,6 +15,7 @@ const AuthContext = createContext<AuthContextType>({
|
|||||||
userRoles: [],
|
userRoles: [],
|
||||||
setAuth: () => { },
|
setAuth: () => { },
|
||||||
isLoading: true, // Default to loading
|
isLoading: true, // Default to loading
|
||||||
|
refreshToken: async () => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
@ -33,6 +35,26 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshToken = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/authentication/refreshtoken', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setAuth(data.accessToken);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
setAuth(null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setAuth(null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Check auth on mount
|
// Check auth on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeAuth = async () => {
|
const initializeAuth = async () => {
|
||||||
@ -55,9 +77,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setAuth(null);
|
setAuth(null);
|
||||||
} finally {
|
|
||||||
setIsLoading(false); // Done loading regardless of outcome
|
|
||||||
}
|
}
|
||||||
|
setIsLoading(false); // Done loading regardless of outcome
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -65,7 +86,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading }}>
|
<AuthContext.Provider value={{ accessToken, userRoles, setAuth, isLoading, refreshToken }}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import Menu from '@mui/material/Menu';
|
|||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
@ -79,6 +80,7 @@ interface LayoutProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Layout = ({ children }: LayoutProps) => {
|
const Layout = ({ children }: LayoutProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); //TODO: Move this to shared utils?
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); //TODO: Move this to shared utils?
|
||||||
const [open, setOpen] = React.useState(!isMobile);
|
const [open, setOpen] = React.useState(!isMobile);
|
||||||
@ -118,7 +120,7 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
const handleRefreshUser = async () => {
|
const handleRefreshUser = async () => {
|
||||||
handleCloseProfileMenu();
|
handleCloseProfileMenu();
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/authentication/refreshtoken', {
|
const response = await customFetch('/api/authentication/refreshtoken', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|
||||||
@ -138,7 +140,7 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
setAuth(null); // Clear context
|
setAuth(null); // Clear context
|
||||||
await fetch('/api/authentication/logout', { method: 'POST', credentials: 'include' });
|
await customFetch('/api/authentication/logout', { method: 'POST', credentials: 'include' });
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
|||||||
import { useForm, Resolver, Controller } from "react-hook-form";
|
import { useForm, Resolver, Controller } from "react-hook-form";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type BouncedEmailEditProps = {
|
type BouncedEmailEditProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -48,6 +49,7 @@ const defaultBouncedEmail: BouncedEmail = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailEditProps) => {
|
const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailEditProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const isNew = bouncedEmail == null;
|
const isNew = bouncedEmail == null;
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
const originalBouncedEmail: BouncedEmail | null = bouncedEmail ? { ...bouncedEmail } : null;
|
const originalBouncedEmail: BouncedEmail | null = bouncedEmail ? { ...bouncedEmail } : null;
|
||||||
@ -73,7 +75,7 @@ const BouncedEmailEdit = ({ open, bouncedEmail, onClose, onSave }: BouncedEmailE
|
|||||||
const method = isNew ? "POST" : "PUT"
|
const method = isNew ? "POST" : "PUT"
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(formData),
|
body: JSON.stringify(formData),
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
|||||||
import { useForm, Controller, Resolver } from "react-hook-form";
|
import { useForm, Controller, Resolver } from "react-hook-form";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type EmailDomainEditProps = {
|
type EmailDomainEditProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -56,6 +57,7 @@ const defaultEmailDomain: EmailDomain = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEditProps) => {
|
const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEditProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const isNew = !emailDomain || emailDomain.id === 0;
|
const isNew = !emailDomain || emailDomain.id === 0;
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null;
|
const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null;
|
||||||
@ -80,7 +82,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
|
|||||||
const method = isNew ? "POST" : "PUT";
|
const method = isNew ? "POST" : "PUT";
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(formData),
|
body: JSON.stringify(formData),
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useState, FormEvent } from 'react';
|
import { useState, FormEvent } from 'react';
|
||||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/material';
|
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/material';
|
||||||
import { Close as CloseIcon } from '@mui/icons-material';
|
import { Close as CloseIcon } from '@mui/icons-material';
|
||||||
import utils from '@/ts/utils';
|
import utils from '@/utils/utils';
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type FormErrors = Record<string, string>;
|
type FormErrors = Record<string, string>;
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ type ForgotPasswordModalProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose }) => {
|
const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose }) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
||||||
const [usernameNotFound, setUsernameNotFound] = useState(false);
|
const [usernameNotFound, setUsernameNotFound] = useState(false);
|
||||||
@ -36,7 +38,7 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
|
|||||||
if (validate()) {
|
if (validate()) {
|
||||||
console.log('Processing forgot password for', username);
|
console.log('Processing forgot password for', username);
|
||||||
const apiUrl = "/api/authentication/generatepasswordrecovery";
|
const apiUrl = "/api/authentication/generatepasswordrecovery";
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username}),
|
body: JSON.stringify({ username}),
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|||||||
import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling
|
import dayjs, { Dayjs } from 'dayjs'; // Import Dayjs for date handling
|
||||||
import utc from 'dayjs/plugin/utc'; // Import the UTC plugin
|
import utc from 'dayjs/plugin/utc'; // Import the UTC plugin
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@ -174,12 +175,12 @@ const schema = yup.object().shape({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const nameIsAvailable = async (id: number, name: string) => {
|
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();
|
const data = await response.json();
|
||||||
return data.available;
|
return data.available;
|
||||||
};
|
};
|
||||||
const getNextAvailableName = async (id: number, name: string) => {
|
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();
|
const data = await response.json();
|
||||||
return data.name;
|
return data.name;
|
||||||
};
|
};
|
||||||
@ -226,6 +227,7 @@ const defaultMailing: Mailing = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
@ -314,7 +316,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const jsonPayload = JSON.stringify(formData);
|
const jsonPayload = JSON.stringify(formData);
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: jsonPayload,
|
body: jsonPayload,
|
||||||
@ -357,7 +359,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make the API call to /api/mailings/test
|
// Make the API call to /api/mailings/test
|
||||||
const response = await fetch("/api/mailings/test", {
|
const response = await customFetch("/api/mailings/test", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -387,7 +389,7 @@ const MailingEdit = ({ open, mailing, onClose, onSave }: MailingEditProps) => {
|
|||||||
|
|
||||||
setTargetSampleLoading(true);
|
setTargetSampleLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/targets/${currentTarget.id}/sample`);
|
const response = await customFetch(`/api/targets/${currentTarget.id}/sample`);
|
||||||
if (!response.ok) throw new Error("Failed to fetch sample data");
|
if (!response.ok) throw new Error("Failed to fetch sample data");
|
||||||
const data: TargetSample = await response.json();
|
const data: TargetSample = await response.json();
|
||||||
setTargetSample(data);
|
setTargetSample(data);
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import TemplateViewer from "@/components/modals/TemplateViewer";
|
|||||||
import TargetSampleModal from "@/components/modals/TargetSampleModal";
|
import TargetSampleModal from "@/components/modals/TargetSampleModal";
|
||||||
import Target from "@/types/target";
|
import Target from "@/types/target";
|
||||||
import Template from "@/types/template";
|
import Template from "@/types/template";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
interface MailingViewProps {
|
interface MailingViewProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -48,6 +49,7 @@ interface MailingEmail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const setupData = useSetupData();
|
const setupData = useSetupData();
|
||||||
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
|
const [templateViewerOpen, setTemplateViewerOpen] = useState<boolean>(false);
|
||||||
const [targetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
const [targetSampleModalOpen, setTargetSampleModalOpen] = useState<boolean>(false);
|
||||||
@ -65,7 +67,7 @@ function MailingView({ open, mailing, onClose }: MailingViewProps) {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const emailsResponse = await fetch(`/api/mailings/${mailing.id}/emails`);
|
const emailsResponse = await customFetch(`/api/mailings/${mailing.id}/emails`);
|
||||||
const emailsData = await emailsResponse.json();
|
const emailsData = await emailsResponse.json();
|
||||||
|
|
||||||
if (emailsData) {
|
if (emailsData) {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import Server from "@/types/server";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type ServerEditProps = {
|
type ServerEditProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -51,6 +52,7 @@ const defaultServer: Server = {
|
|||||||
password: "",
|
password: "",
|
||||||
};
|
};
|
||||||
const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
|
const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const isNew = !server || server.id === 0;
|
const isNew = !server || server.id === 0;
|
||||||
const originalServer: Server | null = server ? { ...server } : null;
|
const originalServer: Server | null = server ? { ...server } : null;
|
||||||
//const [formData, setFormData] = useState<Server>({ ...server });
|
//const [formData, setFormData] = useState<Server>({ ...server });
|
||||||
@ -73,7 +75,7 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
|
|||||||
const method = isNew ? "POST" : "PUT";
|
const method = isNew ? "POST" : "PUT";
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(formData),
|
body: JSON.stringify(formData),
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import TargetColumn from "@/types/targetColumn";
|
|||||||
import TargetSampleColumn from "@/types/targetSampleColumn";
|
import TargetSampleColumn from "@/types/targetSampleColumn";
|
||||||
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
|
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
|
||||||
import { GridColDef } from '@mui/x-data-grid';
|
import { GridColDef } from '@mui/x-data-grid';
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type TargetEditProps = {
|
type TargetEditProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -96,6 +97,7 @@ const useColumnErrors = (errors: any, columns: TargetColumn[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const isNew = !target || target.id === 0;
|
const isNew = !target || target.id === 0;
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
const [targetTested, setTargetTested] = useState<boolean>(false);
|
const [targetTested, setTargetTested] = useState<boolean>(false);
|
||||||
@ -136,7 +138,7 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
|||||||
const method = isNew ? "POST" : "PUT";
|
const method = isNew ? "POST" : "PUT";
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(formData),
|
body: JSON.stringify(formData),
|
||||||
@ -172,7 +174,7 @@ const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => {
|
|||||||
const handleTestTarget = async () => {
|
const handleTestTarget = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/targets/test`, {
|
const response = await customFetch(`/api/targets/test`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import CloseIcon from '@mui/icons-material/Close';
|
|||||||
import TargetSample from "@/types/targetSample";
|
import TargetSample from "@/types/targetSample";
|
||||||
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
|
import TargetSampleDataGrid from "@/components/shared/TargetSampleDataGrid";
|
||||||
import { GridColDef } from '@mui/x-data-grid';
|
import { GridColDef } from '@mui/x-data-grid';
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type TargetSampleModalProps = {
|
type TargetSampleModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -22,6 +23,7 @@ type TargetSampleModalProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TargetSampleModal = ({ open, target, targetSample, onClose }: TargetSampleModalProps) => {
|
const TargetSampleModal = ({ open, target, targetSample, onClose }: TargetSampleModalProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const [localTargetSample, setLocalTargetSample] = useState<TargetSample | null>(null);
|
const [localTargetSample, setLocalTargetSample] = useState<TargetSample | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@ -34,7 +36,7 @@ const TargetSampleModal = ({ open, target, targetSample, onClose }: TargetSample
|
|||||||
else if (target.id) {
|
else if (target.id) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/targets/${target.id}/sample`);
|
const response = await customFetch(`/api/targets/${target.id}/sample`);
|
||||||
if (!response.ok) throw new Error("Failed to fetch sample data");
|
if (!response.ok) throw new Error("Failed to fetch sample data");
|
||||||
const data: TargetSample = await response.json();
|
const data: TargetSample = await response.json();
|
||||||
setLocalTargetSample(data);
|
setLocalTargetSample(data);
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import Editor from "@monaco-editor/react";
|
|||||||
// Assuming these types and context are defined elsewhere
|
// Assuming these types and context are defined elsewhere
|
||||||
import Template from "@/types/template";
|
import Template from "@/types/template";
|
||||||
import { useSetupData } from "@/context/SetupDataContext";
|
import { useSetupData } from "@/context/SetupDataContext";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type TemplateEditProps = {
|
type TemplateEditProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -64,6 +65,7 @@ const defaultTemplate: Template = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) => {
|
const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const isNew = !template || template.id === 0;
|
const isNew = !template || template.id === 0;
|
||||||
const setupData = useSetupData();
|
const setupData = useSetupData();
|
||||||
|
|
||||||
@ -100,7 +102,7 @@ const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) =>
|
|||||||
const method = isNew ? "POST" : "PUT";
|
const method = isNew ? "POST" : "PUT";
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method,
|
method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(formData),
|
body: JSON.stringify(formData),
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import TestEmailList from "@/types/testEmailList";
|
import TestEmailList from "@/types/testEmailList";
|
||||||
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
import { useSetupData, SetupData } from "@/context/SetupDataContext";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type TestEmailListEditProps = {
|
type TestEmailListEditProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -56,6 +57,7 @@ const defaultTestEmailList: TestEmailListForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailListEditProps) => {
|
const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailListEditProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const isNew = !testEmailList || testEmailList.id === 0;
|
const isNew = !testEmailList || testEmailList.id === 0;
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -90,7 +92,7 @@ const TestEmailListEdit = ({ open, testEmailList, onClose, onSave }: TestEmailLi
|
|||||||
const method = isNew ? "POST" : "PUT";
|
const method = isNew ? "POST" : "PUT";
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method,
|
method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(testEmailListDto),
|
body: JSON.stringify(testEmailListDto),
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import UnsubscribeUrl from "@/types/unsubscribeUrl";
|
|||||||
import { useForm, Resolver } from "react-hook-form";
|
import { useForm, Resolver } from "react-hook-form";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
type UnsubscribeUrlEditProps = {
|
type UnsubscribeUrlEditProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -32,6 +33,7 @@ const defaultUnsubscribeUrl: UnsubscribeUrl = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UnsubscribeUrlEdit = ({ open, unsubscribeUrl, onClose, onSave }: UnsubscribeUrlEditProps) => {
|
const UnsubscribeUrlEdit = ({ open, unsubscribeUrl, onClose, onSave }: UnsubscribeUrlEditProps) => {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const isNew = !unsubscribeUrl || unsubscribeUrl.id === 0;
|
const isNew = !unsubscribeUrl || unsubscribeUrl.id === 0;
|
||||||
|
|
||||||
const { register, handleSubmit, reset, formState: { errors } } = useForm<UnsubscribeUrl>({
|
const { register, handleSubmit, reset, formState: { errors } } = useForm<UnsubscribeUrl>({
|
||||||
@ -53,7 +55,7 @@ const UnsubscribeUrlEdit = ({ open, unsubscribeUrl, onClose, onSave }: Unsubscri
|
|||||||
const method = isNew ? "POST" : "PUT";
|
const method = isNew ? "POST" : "PUT";
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(formData),
|
body: JSON.stringify(formData),
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import RefreshIcon from '@mui/icons-material/Refresh';
|
|||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton, FormControlLabel } from '@mui/material';
|
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton, FormControlLabel } from '@mui/material';
|
||||||
import { DataGrid, GridColDef, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
import { DataGrid, GridColDef, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||||
import MailingStatistic from '@/types/mailingStatistic'; // Assuming you'll create this type based on the C# class
|
import MailingStatistic from '@/types/mailingStatistic';
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
function ActiveMailings() {
|
function ActiveMailings() {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
|
||||||
@ -39,7 +41,7 @@ function ActiveMailings() {
|
|||||||
isFetchingRef.current = true;
|
isFetchingRef.current = true;
|
||||||
setMailingsLoading(true);
|
setMailingsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/mailings/status/SD/stats");
|
const response = await customFetch("/api/mailings/status/SD/stats");
|
||||||
const statsData = await response.json();
|
const statsData = await response.json();
|
||||||
if (statsData) {
|
if (statsData) {
|
||||||
setMailingStats(statsData);
|
setMailingStats(statsData);
|
||||||
|
|||||||
@ -7,6 +7,25 @@ import { TitleProvider } from "@/context/TitleContext";
|
|||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
||||||
|
|
||||||
|
import { getWebInstrumentations, initializeFaro } from '@grafana/faro-web-sdk';
|
||||||
|
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
|
||||||
|
|
||||||
|
initializeFaro({
|
||||||
|
url: 'https://faro-collector-prod-us-west-0.grafana.net/collect/4be6b1cb063deee9f9f54fc46da6c6e7',
|
||||||
|
app: {
|
||||||
|
name: 'Surge365 Mass Email',
|
||||||
|
version: '1.0.0',
|
||||||
|
environment: 'production'
|
||||||
|
},
|
||||||
|
|
||||||
|
instrumentations: [
|
||||||
|
// Mandatory, omits default instrumentations otherwise.
|
||||||
|
...getWebInstrumentations(),
|
||||||
|
|
||||||
|
// Tracing package to get end-to-end visibility for HTTP requests.
|
||||||
|
new TracingInstrumentation(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
if (rootElement) {
|
if (rootElement) {
|
||||||
|
|||||||
@ -7,8 +7,10 @@ import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Circ
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton, GridDeleteIcon } from '@mui/x-data-grid';
|
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton, GridDeleteIcon } from '@mui/x-data-grid';
|
||||||
import BouncedEmail from '@/types/bouncedEmail';
|
import BouncedEmail from '@/types/bouncedEmail';
|
||||||
import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit";
|
import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
function BouncedEmails() {
|
function BouncedEmails() {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
@ -54,7 +56,7 @@ function BouncedEmails() {
|
|||||||
const method = "DELETE";
|
const method = "DELETE";
|
||||||
//setLoading(true);
|
//setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await customFetch(apiUrl, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,9 +7,11 @@ import { Box, useTheme, CircularProgress, IconButton } from '@mui/material';
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
import { DataGrid, GridColDef, GridRenderCellParams, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||||
import Mailing from '@/types/mailing';
|
import Mailing from '@/types/mailing';
|
||||||
import MailingEdit from "@/components/modals/MailingEdit";
|
import MailingEdit from "@/components/modals/MailingEdit";
|
||||||
import MailingView from "@/components/modals/MailingView"; // Assume this is a new read-only view component
|
import MailingView from "@/components/modals/MailingView";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
function CancelledMailings() {
|
function CancelledMailings() {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
|
|
||||||
@ -51,7 +53,7 @@ function CancelledMailings() {
|
|||||||
|
|
||||||
const reloadMailings = async () => {
|
const reloadMailings = async () => {
|
||||||
setMailingsLoading(true);
|
setMailingsLoading(true);
|
||||||
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);
|
||||||
|
|||||||
@ -9,8 +9,10 @@ import MailingStatistic from '@/types/mailingStatistic';
|
|||||||
import Mailing from '@/types/mailing';
|
import Mailing from '@/types/mailing';
|
||||||
import MailingView from "@/components/modals/MailingView";
|
import MailingView from "@/components/modals/MailingView";
|
||||||
import MailingEdit from "@/components/modals/MailingEdit";
|
import MailingEdit from "@/components/modals/MailingEdit";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
function CompletedMailings() {
|
function CompletedMailings() {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
|
||||||
@ -93,7 +95,7 @@ function CompletedMailings() {
|
|||||||
if (params.length > 0) {
|
if (params.length > 0) {
|
||||||
url += `?${params.join('&')}`;
|
url += `?${params.join('&')}`;
|
||||||
}
|
}
|
||||||
const response = await fetch(url);
|
const response = await customFetch(url);
|
||||||
const statsData = await response.json();
|
const statsData = await response.json();
|
||||||
if (statsData) {
|
if (statsData) {
|
||||||
setMailingStats(statsData);
|
setMailingStats(statsData);
|
||||||
@ -109,7 +111,7 @@ function CompletedMailings() {
|
|||||||
|
|
||||||
const fetchMailingDetails = async (mailingId: number) => {
|
const fetchMailingDetails = async (mailingId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/mailings/${mailingId}`);
|
const response = await customFetch(`/api/mailings/${mailingId}`);
|
||||||
const mailingData = await response.json();
|
const mailingData = await response.json();
|
||||||
if (mailingData) {
|
if (mailingData) {
|
||||||
return mailingData;
|
return mailingData;
|
||||||
|
|||||||
@ -8,8 +8,10 @@ import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Butt
|
|||||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||||
import EmailDomain from '@/types/emailDomain';
|
import EmailDomain from '@/types/emailDomain';
|
||||||
import EmailDomainEdit from "@/components/modals/EmailDomainEdit";
|
import EmailDomainEdit from "@/components/modals/EmailDomainEdit";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
function EmailDomains() {
|
function EmailDomains() {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
@ -69,7 +71,7 @@ function EmailDomains() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const loadEmailDomainsWithPasswords = async () => {
|
const loadEmailDomainsWithPasswords = async () => {
|
||||||
const response = await fetch("/api/emailDomains/GetAll?activeOnly=false&returnPassword=true");
|
const response = await customFetch("/api/emailDomains/GetAll?activeOnly=false&returnPassword=true");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setEmailDomainsWithPasswords(data);
|
setEmailDomainsWithPasswords(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,10 @@ import Mailing from '@/types/mailing';
|
|||||||
//import Template from '@/types/template';
|
//import Template from '@/types/template';
|
||||||
import MailingEdit from "@/components/modals/MailingEdit";
|
import MailingEdit from "@/components/modals/MailingEdit";
|
||||||
import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
|
import ConfirmationDialog from "@/components/modals/ConfirmationDialog";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
function NewMailings() {
|
function NewMailings() {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
@ -56,7 +58,7 @@ function NewMailings() {
|
|||||||
const reloadMailings = async () => {
|
const reloadMailings = async () => {
|
||||||
setMailingsLoading(true);
|
setMailingsLoading(true);
|
||||||
|
|
||||||
const mailingsResponse = await fetch("/api/mailings/status/ed");
|
const mailingsResponse = await customFetch("/api/mailings/status/ed");
|
||||||
const mailingsData = await mailingsResponse.json();
|
const mailingsData = await mailingsResponse.json();
|
||||||
if (mailingsData) {
|
if (mailingsData) {
|
||||||
setMailings(mailingsData);
|
setMailings(mailingsData);
|
||||||
@ -94,7 +96,7 @@ function NewMailings() {
|
|||||||
if (!mailingToCancel) return;
|
if (!mailingToCancel) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' });
|
const response = await customFetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
|
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -9,8 +9,10 @@ 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 isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
|
||||||
@ -79,7 +81,7 @@ function ScheduleMailings() {
|
|||||||
|
|
||||||
const reloadMailings = async () => {
|
const reloadMailings = async () => {
|
||||||
setMailingsLoading(true);
|
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();
|
const mailingsData = await mailingsResponse.json();
|
||||||
if (mailingsData) {
|
if (mailingsData) {
|
||||||
setMailings(mailingsData);
|
setMailings(mailingsData);
|
||||||
@ -111,7 +113,7 @@ function ScheduleMailings() {
|
|||||||
if (!mailingToCancel) return;
|
if (!mailingToCancel) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' });
|
const response = await customFetch(`/api/mailings/${mailingToCancel.id}/cancel`, { method: 'POST' });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
|
setMailings((prev) => prev.filter(m => m.id !== mailingToCancel.id));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -7,12 +7,14 @@ import RefreshIcon from '@mui/icons-material/Refresh';
|
|||||||
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
|
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
|
||||||
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
|
||||||
import { Lock, LockOpen } from "@mui/icons-material";
|
import { Lock, LockOpen } from "@mui/icons-material";
|
||||||
//import utils from '@/ts/utils';
|
//import utils from '@/utils/utils';
|
||||||
import Server from '@/types/server';
|
import Server from '@/types/server';
|
||||||
import ServerEdit from "@/components/modals/ServerEdit";
|
import ServerEdit from "@/components/modals/ServerEdit";
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
|
|
||||||
function Servers() {
|
function Servers() {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const setupData: SetupData = useSetupData();
|
const setupData: SetupData = useSetupData();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
@ -31,7 +33,7 @@ function Servers() {
|
|||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
setIsPasswordVisible(true);
|
setIsPasswordVisible(true);
|
||||||
const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=true");
|
const serversResponse = await customFetch("/api/servers/GetAll?activeOnly=false&returnPassword=true");
|
||||||
const serversData = await serversResponse.json();
|
const serversData = await serversResponse.json();
|
||||||
setServersWithPasswords(serversData);
|
setServersWithPasswords(serversData);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import dayjs from 'dayjs'; // Import Dayjs for date handling
|
|||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import MailingStatistic from '@/types/mailingStatistic';
|
import MailingStatistic from '@/types/mailingStatistic';
|
||||||
|
import { useCustomFetch } from "@/utils/customFetch";
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
export default function RecentMailingStatsChart({ days = 7 }: { days?: number }) {
|
export default function RecentMailingStatsChart({ days = 7 }: { days?: number }) {
|
||||||
|
const customFetch = useCustomFetch();
|
||||||
const [stats, setStats] = useState<MailingStatistic[]>([]);
|
const [stats, setStats] = useState<MailingStatistic[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@ -22,7 +24,7 @@ export default function RecentMailingStatsChart({ days = 7 }: { days?: number })
|
|||||||
|
|
||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/mailings/status/s%2Csd/stats?startDate=${startDateString}&endDate=${endDateString}`);
|
const response = await customFetch(`/api/mailings/status/s%2Csd/stats?startDate=${startDateString}&endDate=${endDateString}`);
|
||||||
const data: MailingStatistic[] = await response.json();
|
const data: MailingStatistic[] = await response.json();
|
||||||
setStats(data);
|
setStats(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
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?
|
/*The following may not be needed any longer?
|
||||||
**TODO: WebMethod should be changed to mimic fetch command but add in auth headers?
|
**TODO: WebMethod should be changed to mimic fetch command but add in auth headers?
|
||||||
fetch('/api/protected-endpoint', {
|
customFetch('/api/protected-endpoint', {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
|
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
|
||||||
}
|
}
|
||||||
@ -98,7 +98,7 @@ const utils = {
|
|||||||
// //const timeoutId = setTimeout(() => controller.abort(), timeout);
|
// //const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
// setTimeout(() => controller.abort(), timeout);
|
// setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
// const response = await fetch(url, {
|
// const response = await customFetch(url, {
|
||||||
// method: httpMethod,
|
// method: httpMethod,
|
||||||
// headers,
|
// headers,
|
||||||
// body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)),
|
// body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)),
|
||||||
Loading…
x
Reference in New Issue
Block a user