Refactor authentication and layout structure

- Updated `AuthenticationController` to handle refresh tokens and store them in the database.
- Modified `IAuthService` and `IUserRepository` to support new authentication logic using refresh tokens.
- Removed `GenerateTokens` method and added `SaveRefreshToken` method.
- Adjusted database mappings in `UserMap`.
- Enhanced `AuthService` to implement new authentication flow.
- Removed `LayoutAdminLTE.tsx`, simplifying the layout structure.
- Improved theme initialization in `App.tsx` for persistent settings.
- Cleaned up component files (`Home`, `Login`, `Targets`, `Templates`) by removing boilerplate code.
- Added `eslint.config.js` for ESLint configuration.
- Introduced `ThemeWrapper.tsx` for managing theme settings (implementation commented out).
This commit is contained in:
David Headrick 2025-04-07 20:28:52 -05:00
parent f5b1fe6397
commit 5e85b9e596
20 changed files with 185 additions and 608 deletions

View File

@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.API.Controllers;
using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.API.Controllers
{
@ -38,6 +39,7 @@ namespace Surge365.MassEmailReact.API.Controllers
else if(authResponse.data == null)
return Unauthorized(new { message = "Invalid credentials" });
//TODO: Store refresh token in DB
var cookieOptions = new CookieOptions
{
HttpOnly = true, // Prevents JavaScript access (mitigates XSS)
@ -58,15 +60,12 @@ namespace Surge365.MassEmailReact.API.Controllers
if (string.IsNullOrWhiteSpace(refreshToken))
return Unauthorized("Invalid refresh token");
Guid? userId = Guid.Parse("B077E02E-7383-4942-B57D-F2DFA9D33B8E");//TODO: Lookup user in session by refresh token
if (userId == null)
{
return Unauthorized("Invalid refresh token");
}
var authResponse = await _authService.Authenticate(refreshToken);
if (!authResponse.authenticated)
return Unauthorized(new { message = authResponse.errorMessage });
else if (authResponse.data == null)
return Unauthorized(new { message = "Invalid credentials" });
var tokens = await _authService.GenerateTokens(userId.Value, refreshToken);
if(tokens == null)
return Unauthorized("Invalid refresh token");
var cookieOptions = new CookieOptions
{
HttpOnly = true,
@ -74,9 +73,9 @@ namespace Surge365.MassEmailReact.API.Controllers
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(7)
};
Response.Cookies.Append("refreshToken", tokens.Value.refreshToken, cookieOptions);
Response.Cookies.Append("refreshToken", authResponse.data.Value.refreshToken, cookieOptions);
return Ok(new { accessToken = tokens.Value.accessToken });
return Ok(new { accessToken = authResponse.data.Value.accessToken });
}
[HttpPost("generatepasswordrecovery")]
public IActionResult GeneratePasswordRecovery([FromBody] GeneratePasswordRecoveryRequest request)

View File

@ -5,6 +5,6 @@ namespace Surge365.MassEmailReact.Application.Interfaces
public interface IAuthService
{
Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password);
Task<(string accessToken, string refreshToken)?> GenerateTokens(Guid userId, string refreshToken);
Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string refreshToken);
}
}

View File

@ -10,10 +10,12 @@ namespace Surge365.MassEmailReact.Application.Interfaces
public interface IUserRepository
{
Task<(User? user, string message)> Authenticate(string username, string password);
bool Authenticate(Guid userId, string refreshToken);
Task<User?> Authenticate(string refreshToken);
Task<User?> GetByUsername(string username);
Task<User?> GetByKey(int userKey);
Task<User?> GetById(Guid userId);
Task<List<User>> GetAll(bool activeOnly = true);
Task<bool> SaveRefreshToken(Guid userId, string refreshToken, string? previousToken = "");
}
}

View File

@ -13,7 +13,7 @@ namespace Surge365.MassEmailReact.Infrastructure.DapperMaps
public UserMap()
{
Map(u => u.UserKey).ToColumn("login_key");
Map(u => u.UserId).ToColumn("session_activity_id"); // Assuming this is the Guid mapping
Map(u => u.UserId).ToColumn("login_id");
Map(u => u.Username).ToColumn("username");
Map(u => u.FirstName).ToColumn("first_name");
Map(u => u.MiddleInitial).ToColumn("middle_initial");

View File

@ -16,6 +16,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
{
private readonly IConfiguration _config;
private const string _connectionStringName = "Marketing.ConnectionString";
private const string _refreshTokenConnectionStringName = "MassEmail.ConnectionString";
public UserRepository(IConfiguration config)
{
@ -31,6 +32,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
}
private string ConnectionString => _config.GetConnectionString(_connectionStringName) ?? "";
private string RefreshTokenConnectionString => _config.GetConnectionString(_refreshTokenConnectionStringName) ?? "";
public async Task<(User? user, string message)> Authenticate(string username, string password)
{
@ -60,10 +62,24 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
return (null, responseMessage);
}
public bool Authenticate(Guid userId, string refreshToken)
public async Task<User?> Authenticate(string refreshToken)
{
// TODO: Validate refresh token
return true;
using var connection = new SqlConnection(RefreshTokenConnectionString);
var parameters = new DynamicParameters();
parameters.Add("@@token_hash", HashToken(refreshToken));
parameters.Add("@valid", dbType: DbType.Boolean, direction: ParameterDirection.Output);
parameters.Add("@user_guid", dbType: DbType.Guid, direction: ParameterDirection.Output);
await connection.ExecuteAsync(
"mem_validate_user_refresh_token",
parameters,
commandType: CommandType.StoredProcedure
);
if (!parameters.Get<bool>("@valid"))
return null;
return await GetById(parameters.Get<Guid>("@user_guid"));
}
public async Task<User?> GetByUsername(string username)
@ -110,5 +126,42 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
);
return users.AsList();
}
public async Task<bool> SaveRefreshToken(Guid userId, string token, string? previousToken = "")
{
using var connection = new SqlConnection(RefreshTokenConnectionString);
var parameters = new DynamicParameters();
parameters.Add("@user_guid", userId);
parameters.Add("@token_hash", HashToken(token));
parameters.Add("@previous_token_hash", HashToken(previousToken ?? ""));
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await connection.ExecuteAsync(
"mem_save_user_refresh_token",
parameters,
commandType: CommandType.StoredProcedure
);
return parameters.Get<bool>("@success");
}
private string HashToken(string token)
{
if (string.IsNullOrEmpty(token))
{
return "";
}
using (var sha256 = System.Security.Cryptography.SHA256.Create())
{
// Convert the token string to bytes
byte[] tokenBytes = System.Text.Encoding.UTF8.GetBytes(token);
// Compute the hash
byte[] hashBytes = sha256.ComputeHash(tokenBytes);
// Convert the hash to a hexadecimal string
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
}
}
}

View File

@ -26,6 +26,19 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
_userRepository = userRepository;
_config = config;
}
public async Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string refreshToken)
{
var user = await _userRepository.Authenticate(refreshToken);
if (user == null)
return (false, null, "Not authenticated");
var tokenResponse = await GenerateTokens(user.UserId, refreshToken);
if(tokenResponse == null)
return (false, null, "Error generating tokens");
return (true, tokenResponse, "");
}
public async Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password)
{
@ -56,17 +69,14 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
var token = tokenHandler.CreateToken(tokenDescriptor);
var accessToken = tokenHandler.WriteToken(token);
var refreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
//_userRepository.SaveRefreshToken(userId.Value, refreshToken); // TODO: Store refresh token in DB
string refreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
if (!await _userRepository.SaveRefreshToken(authResponse.user.UserId, refreshToken))
return (false, null, "Error saving token");
return (true, (authResponse.user, accessToken, refreshToken), "");
}
public async Task<(string accessToken, string refreshToken)?> GenerateTokens(Guid userId, string refreshToken)
private async Task<(User user, string accessToken, string refreshToken)?> GenerateTokens(Guid userId, string previousRefreshToken)
{
if (!_userRepository.Authenticate(userId, refreshToken))
{
return null;
}
var user = await _userRepository.GetById(userId);
if (user == null)
{
@ -82,7 +92,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
claims.AddRange(user.Roles.Select(role => new Claim(ClaimTypes.Role, role)));
//TODO: Look update User
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
@ -96,9 +106,10 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
var token = tokenHandler.CreateToken(tokenDescriptor);
var accessToken = tokenHandler.WriteToken(token);
var newRefreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
//_userRepository.SaveRefreshToken(userId.Value, newRefreshToken); // TODO: Store refresh token in DB
if (!await _userRepository.SaveRefreshToken(user.UserId, newRefreshToken, previousRefreshToken))
return null;
return (accessToken, newRefreshToken);
return (user, accessToken, newRefreshToken);
}
}
}

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect } from "react";
import { useNavigate, useLocation } from 'react-router-dom';
import utils from '@/ts/utils';
import { useAuth } from '@/components/auth/AuthContext';

View File

@ -79,13 +79,13 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.1)', // Optional: light overlay
zIndex: 9999, // Ensure its above everything
zIndex: 9999, // Ensure its above everything
}}
>
<CircularProgress
size={80} // Larger spinner
thickness={4} // Slightly thicker for visibility
sx={{ color: 'primary.main' }} // Use themes primary color
sx={{ color: 'primary.main' }} // Use themes primary color
/>
<Typography
variant="h6"

View File

@ -1,101 +0,0 @@
import React from 'react';
import AdminLTELogo from 'admin-lte/dist/assets/img/AdminLTELogo.png';
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className="app-wrapper">
{/* Header */}
<nav className="app-header navbar navbar-expand bg-body">
<div className="container-fluid">
{/* Start Navbar Links */}
<ul className="navbar-nav">
<li className="nav-item">
<a className="nav-link" data-lte-toggle="sidebar" href="#" role="button">
<i className="bi bi-list"></i>
</a>
</li>
<li className="nav-item d-none d-md-block">
<a href="./home" className="nav-link">Home</a>
</li>
<li className="nav-item d-none d-md-block">
<a href="#" className="nav-link">TODO</a>
</li>
</ul>
{/* End Navbar Links */}
<ul className="navbar-nav ms-auto">
<li className="nav-item">
<a className="nav-link" data-widget="navbar-search" href="#" role="button">
<i className="bi bi-search"></i>
</a>
</li>
{/* Additional navbar items (messages, notifications, user menu) can go here */}
</ul>
</div>
</nav>
{/* Sidebar */}
<aside className="app-sidebar bg-body-secondary shadow" data-bs-theme="dark">
<div className="sidebar-brand">
<a href="./home" className="brand-link">
<img
src={AdminLTELogo}
alt="AdminLTE Logo"
className="brand-image opacity-75 shadow"
/>
<span className="brand-text fw-light">AdminLTE 4</span>
</a>
</div>
<div className="sidebar-wrapper">
<nav className="mt-2">
<ul
className="nav sidebar-menu flex-column"
data-lte-toggle="treeview"
role="menu"
data-accordion="false"
>
<li className="nav-item menu-open">
<a href="./targets" className="nav-link active">
<i className="nav-icon bi bi-speedometer"></i>
<p>
Targets
<i className="nav-arrow bi bi-chevron-right"></i>
</p>
</a>
</li>
<li className="nav-item menu-open">
<a href="./templates" className="nav-link active">
<i className="nav-icon bi bi-speedometer"></i>
<p>
Templates
<i className="nav-arrow bi bi-chevron-right"></i>
</p>
</a>
</li>
{/* Additional sidebar menu items can be added here */}
</ul>
</nav>
</div>
</aside>
{/* Main Content Area */}
<main className="app-main">
{children}
</main>
{/* Footer */}
<footer className="app-footer">
<div className="float-end d-none d-sm-inline">Version 0.0.1</div>
<strong>
Copyright &copy; 2025&nbsp;
<a href="https://adminlte.io" className="text-decoration-none">Surge365</a>.
</strong>
All rights reserved.
</footer>
</div>
);
};
export default Layout;

View File

@ -69,7 +69,10 @@ const getTheme = (mode: 'light' | 'dark'): Theme => {
};
const App = () => {
const [mode, setMode] = React.useState<'light' | 'dark'>('light');
const [mode, setMode] = React.useState<'light' | 'dark'>(() => {
const savedMode = localStorage.getItem('mui-mode');
return (savedMode === 'light' || savedMode === 'dark') ? savedMode : 'light';
});
const colorMode = React.useMemo(
() => ({

View File

@ -1,7 +1,5 @@
// src/components/pages/Home.tsx
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Grid2 from '@mui/material/Grid2'; // v6 Grid2
import RecentMailingStatsChart from '@/components/widgets/RecentMailingStatsChart';
@ -10,9 +8,6 @@ const Home = () => {
<Box sx={{
position: 'relative', left: 0, right: 0, height: "calc(100vh - 124px)", maxHeight: "700px", overflow: "hidden",
}}>
<Typography variant="h4" component="h1" gutterBottom>
Sent Mailing Statistics
</Typography>
<RecentMailingStatsChart days={14} />
</Box>
);

View File

@ -1,24 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@/css/main.css'
import App from '@/components/pages/App'
import '@/config/constants';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'admin-lte/dist/css/adminlte.min.css';
import 'font-awesome/css/font-awesome.min.css';
/*import 'ionicons/dist/css/ionicons.min.css';*/
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
import 'admin-lte/dist/js/adminlte.min.js';
const rootElement = document.getElementById('root');
if (rootElement) {
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>
);
} else {
throw new Error('Root element not found');
}

View File

@ -1,53 +0,0 @@
// App.tsx or main routing component
//import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Layout from '@/components/layouts/Layout';
import LayoutLogin from '@/components/layouts/LayoutLogin';
import Home from '@/components/pages/Home';
import Login from '@/components/pages/Login';
import Targets from '@/components/pages/Targets';
import Templates from '@/components/pages/Templates';
const App = () => {
return (
<Router basename="/">
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route
path="/home"
element={
<Layout>
<Home />
</Layout>
}
/>
<Route
path="/targets"
element={
<Layout>
<Targets />
</Layout>
}
/>
<Route
path="/templates"
element={
<Layout>
<Templates />
</Layout>
}
/>
<Route
path="/login"
element={
<LayoutLogin>
<Login />
</LayoutLogin>
}
/>
</Routes>
</Router>
);
};
export default App;

View File

@ -1,63 +0,0 @@
import React from 'react';
const Home: React.FC = () => {
return (
<div>
{/* Content Header */}
<div className="app-content-header">
<div className="container-fluid">
<div className="row">
<div className="col-sm-6">
<h3 className="mb-0">Dashboard</h3>
</div>
<div className="col-sm-6">
<ol className="breadcrumb float-sm-end">
<li className="breadcrumb-item">
<a href="#">Home</a>
</li>
<li className="breadcrumb-item active" aria-current="page">
Dashboard
</li>
</ol>
</div>
</div>
</div>
</div>
{/* Page-specific Content */}
<div className="app-content">
<div className="container-fluid">
<div className="row">
{/* Example: Small Box Widget 1 */}
<div className="col-lg-3 col-6">
<div className="small-box text-bg-primary">
<div className="inner">
<h3>150</h3>
<p>New Orders</p>
</div>
<svg
className="small-box-icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"></path>
</svg>
<a
href="#"
className="small-box-footer link-light link-underline-opacity-0 link-underline-opacity-50-hover"
>
More info <i className="bi bi-link-45deg"></i>
</a>
</div>
</div>
{/* Additional small boxes, charts, or other widgets can be included here */}
</div>
{/* You can also add more rows for charts, direct chat, etc. */}
</div>
</div>
</div>
);
};
export default Home;

View File

@ -1,199 +0,0 @@
import { useState } from 'react';
import { Button, Form, Spinner } from 'react-bootstrap';
import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth';
//import { Helmet, HelmetProvider } from 'react-helmet-async';
import utils from '@/ts/utils.ts';
import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal';
type SpinnerState = Record<string, boolean>;
type FormErrors = Record<string, string>;
function Login() {
const [isLoading, setIsLoading] = useState(false);
const [spinners, setSpinnersState] = useState<SpinnerState>({});
const [formErrors, setFormErrors] = useState<FormErrors>({});
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
//const [user, setUser] = useState<User | null>(null);
const [loginError, setLoginError] = useState<boolean>(false);
const [loginErrorMessage, setLoginErrorMessage] = useState<string>('');
//const setSpinners = (newValues: Partial<SpinnerState>) => {
// setSpinnersState((prevSpinners) => ({
// ...prevSpinners,
// ...newValues,
// }));
//};
const setSpinners = (newValues: Partial<SpinnerState>) => {
setSpinnersState((prevSpinners) => {
const updatedSpinners: SpinnerState = { ...prevSpinners };
for (const key in newValues) {
if (newValues[key] !== undefined) {
updatedSpinners[key] = newValues[key] as boolean;
}
}
return updatedSpinners;
});
};
const handleCloseForgotPasswordModal = () => {
setShowForgotPasswordModal(false);
};
const validateLoginForm = () => {
setFormErrors({});
const errors: FormErrors = {};
if (!username.trim()) {
errors.username = 'Username is required';
//} else if (!/\S+@\S+\.\S+/.test(email)) {
// errors.email = 'Invalid email address';
}
if (!password.trim()) {
errors.password = 'Password is required';
}
if (Object.keys(errors).length > 0) {
setFormErrors(errors);
}
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
spinners.Login = true;
setSpinners(spinners);
validateLoginForm();
if (Object.keys(formErrors).length > 0) return;
//setUser(null);
setLoginError(false);
setLoginErrorMessage('');
let loggedInUser: User | null = null;
let hadLoginError: boolean = false;
let hadLoginErrorMessage: string = '';
await utils.webMethod<AuthResponse>({
methodPage: 'authentication',
methodName: 'authenticate',
parameters: { username, password },
success: (json: AuthResponse) => {
try {
loggedInUser = json.user;
//setUser(loggedInUser);
}
catch {
const errorMsg: string = "Unexpected Error";
hadLoginError = true;
hadLoginErrorMessage = errorMsg;
}
},
error: (err: unknown) => {
let errorMsg: string = "Unexpected Error";
if (isAuthErrorResponse(err)) {
if (err && err as AuthErrorResponse) {
if (err.data) {
if (err.data.message)
errorMsg = err.data.message;
}
console.error(errorMsg);
setLoginErrorMessage(errorMsg);
}
}
hadLoginError = true;
hadLoginErrorMessage = errorMsg;
}
});
if (hadLoginError) {
setLoginErrorMessage(hadLoginErrorMessage);
setLoginError(true);
setIsLoading(false);
spinners.Login = false;
setSpinners(spinners);
return;
}
if (loggedInUser == null) {
setLoginError(true);
setIsLoading(false);
spinners.Login = false;
setSpinners(spinners);
} else {
await finishUserLogin(loggedInUser);
}
};
const finishUserLogin = async (loggedInUser: User) => {
setIsLoading(false);
spinners.Login = false;
spinners.LoginWithPasskey = false;
setSpinners(spinners);
utils.localStorage("session_currentUser", loggedInUser);
const redirectUrl = utils.sessionStorage("redirect_url");
if (redirectUrl) {
utils.sessionStorage("redirect_url", null);
document.location.href = redirectUrl;
} else {
document.location.href = '/home';
}
};
return (
<div className="container">
<div className="row text-center mt-5">
<h1>surge365 - React</h1>
</div>
<div className="row text-center" style={{ maxWidth: '400px', margin: 'auto' }}>
<h3 className="form-signin-heading mt-3 mb-1">Please sign in</h3>
<Form id="frmLogin" onSubmit={handleLogin}>
{loginError && (
<Form.Label style={{ color: 'red' }}>{loginErrorMessage ?? "Login error"}</Form.Label>
)}
<Form.Group className="mb-3" controlId="txtUsernamel">
<Form.Label className="visually-hidden">Username</Form.Label>
<Form.Control
type="username"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoFocus
size="sm"
/>
{spinners.Username && <Spinner animation="border" size="sm" />}
</Form.Group>
<Form.Group className="mb-3" controlId="txtPassword">
<Form.Label className="visually-hidden">Password</Form.Label>
<Form.Control
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
size="sm"
/>
</Form.Group>
<Button className="bg-orange w-100" type="submit" disabled={isLoading}>
{spinners.Login && <Spinner animation="border" size="sm" className="me-2" />}
{isLoading && spinners.Login ? 'Signing in...' : 'Sign in'}
</Button>
<Button variant="secondary" className="w-100 mt-2" onClick={() => setShowForgotPasswordModal(true)}>
Forgot Password
</Button>
</Form>
</div>
<ForgotPasswordModal show={showForgotPasswordModal} onClose={handleCloseForgotPasswordModal} />
</div>
);
}
export default Login;

View File

@ -1,63 +0,0 @@
import React from 'react';
const Targets: React.FC = () => {
return (
<div>
{/* Content Header */}
<div className="app-content-header">
<div className="container-fluid">
<div className="row">
<div className="col-sm-6">
<h3 className="mb-0">Targets</h3>
</div>
<div className="col-sm-6">
<ol className="breadcrumb float-sm-end">
<li className="breadcrumb-item">
<a href="#">Home</a>
</li>
<li className="breadcrumb-item active" aria-current="page">
Targets
</li>
</ol>
</div>
</div>
</div>
</div>
{/* Page-specific Content */}
<div className="app-content">
<div className="container-fluid">
<div className="row">
{/* Example: Small Box Widget 1 */}
<div className="col-lg-3 col-6">
<div className="small-box text-bg-primary">
<div className="inner">
<h3>150</h3>
<p>Targets</p>
</div>
<svg
className="small-box-icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"></path>
</svg>
<a
href="#"
className="small-box-footer link-light link-underline-opacity-0 link-underline-opacity-50-hover"
>
More info <i className="bi bi-link-45deg"></i>
</a>
</div>
</div>
{/* Additional small boxes, charts, or other widgets can be included here */}
</div>
{/* You can also add more rows for charts, direct chat, etc. */}
</div>
</div>
</div>
);
};
export default Targets;

View File

@ -1,63 +0,0 @@
import React from 'react';
const Templates: React.FC = () => {
return (
<div>
{/* Content Header */}
<div className="app-content-header">
<div className="container-fluid">
<div className="row">
<div className="col-sm-6">
<h3 className="mb-0">Templates</h3>
</div>
<div className="col-sm-6">
<ol className="breadcrumb float-sm-end">
<li className="breadcrumb-item">
<a href="./home">Home</a>
</li>
<li className="breadcrumb-item active" aria-current="page">
Templates
</li>
</ol>
</div>
</div>
</div>
</div>
{/* Page-specific Content */}
<div className="app-content">
<div className="container-fluid">
<div className="row">
{/* Example: Small Box Widget 1 */}
<div className="col-lg-3 col-6">
<div className="small-box text-bg-primary">
<div className="inner">
<h3>150</h3>
<p>Templates</p>
</div>
<svg
className="small-box-icon"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2.25 2.25a.75.75 0 000 1.5h1.386c.17 0 .318.114.362.278l2.558 9.592a3.752 3.752 0 00-2.806 3.63c0 .414.336.75.75.75h15.75a.75.75 0 000-1.5H5.378A2.25 2.25 0 017.5 15h11.218a.75.75 0 00.674-.421 60.358 60.358 0 002.96-7.228.75.75 0 00-.525-.965A60.864 60.864 0 005.68 4.509l-.232-.867A1.875 1.875 0 003.636 2.25H2.25zM3.75 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM16.5 20.25a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0z"></path>
</svg>
<a
href="#"
className="small-box-footer link-light link-underline-opacity-0 link-underline-opacity-50-hover"
>
More info <i className="bi bi-link-45deg"></i>
</a>
</div>
</div>
{/* Additional small boxes, charts, or other widgets can be included here */}
</div>
{/* You can also add more rows for charts, direct chat, etc. */}
</div>
</div>
</div>
);
};
export default Templates;

View File

@ -0,0 +1,10 @@
// This file allows you to configure ESLint according to your project's needs, so that you
// can control the strictness of the linter, the plugins to use, and more.
// For more information about configuring ESLint, visit https://eslint.org/docs/user-guide/configuring/
module.exports = [
{
rules: {}
}
];

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Box } from '@mui/material';
import { Box, Typography } from '@mui/material';
import { BarChart } from '@mui/x-charts/BarChart';
import dayjs from 'dayjs';
import MailingStatistic from '@/types/mailingStatistic';
@ -8,9 +8,11 @@ export default function RecentMailingStatsChart({ days = 7 }: { days?: number })
const [stats, setStats] = useState<MailingStatistic[]>([]);
const [loading, setLoading] = useState(true);
var startDate = dayjs().subtract(days, 'day').format('YYYY-MM-DD');
var endDate = dayjs().format('YYYY-MM-DD');
const startDate = dayjs().subtract(days, 'day').format('YYYY-MM-DD');
const endDate = dayjs().format('YYYY-MM-DD');
//startDate = '2025-02-12';
//endDate = '2025-02-26'
const fetchStats = async () => {
try {
const response = await fetch(`/api/mailings/status/s%2Csd/stats?startDate=${startDate}&endDate=${endDate}`);
@ -33,10 +35,6 @@ export default function RecentMailingStatsChart({ days = 7 }: { days?: number })
return <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>Loading...</Box>;
}
if (!stats.length) {
return <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>No data available</Box>;
}
// Aggregate stats by date
const aggregatedStats = stats.reduce((acc, stat) => {
const dateStr = stat.sentDate ? dayjs(stat.sentDate).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
@ -72,7 +70,12 @@ export default function RecentMailingStatsChart({ days = 7 }: { days?: number })
));
return (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%' }}>
<Typography variant="h5" component="h1" gutterBottom>
Sent Mailings - Last 14 Days
</Typography>
<BarChart
title="Sent Mailings - Last 14 Days"
margin={{
left: 80,
right: 80,
@ -104,5 +107,6 @@ export default function RecentMailingStatsChart({ days = 7 }: { days?: number })
},
]}
/>
</Box>
);
}

View File

@ -0,0 +1,66 @@
//// src/components/ThemeWrapper.tsx
//import React from 'react';
//import { createTheme, ThemeProvider, Theme } from '@mui/material/styles';
//import CssBaseline from '@mui/material/CssBaseline';
//import { ColorModeContext } from '@/theme/theme';
//interface ThemeWrapperProps {
// children: React.ReactNode;
//}
//const getTheme = (mode: 'light' | 'dark'): Theme => {
// console.log('getTheme called with mode:', mode); // Debug log
// return createTheme({
// palette: {
// mode,
// },
// cssVariables: {
// colorSchemeSelector: 'class',
// },
// colorSchemes: {
// light: true,
// dark: true,
// },
// components: {
// MuiAutocomplete: {
// defaultProps: {
// handleHomeEndKeys: false,
// },
// },
// },
// });
//};
//// src/components/ThemeWrapper.tsx
//export const ThemeWrapper: React.FC<ThemeWrapperProps> = ({ children }) => {
// const [mode, setMode] = React.useState<'light' | 'dark'>(() => {
// const savedMode = localStorage.getItem('themeMode');
// return (savedMode === 'light' || savedMode === 'dark') ? savedMode : 'light';
// });
// const colorMode = React.useMemo(
// () => ({
// mode, // Include mode in the context
// toggleColorMode: () => {
// setMode((prevMode) => {
// const newMode = prevMode === 'light' ? 'dark' : 'light';
// localStorage.setItem('themeMode', newMode);
// console.log('Toggled mode to:', newMode);
// return newMode;
// });
// },
// }),
// [mode] // Add mode as a dependency
// );
// const theme = React.useMemo(() => getTheme(mode), [mode]);
// return (
// <ColorModeContext.Provider value={colorMode}>
// <ThemeProvider theme={theme}>
// <CssBaseline />
// {children}
// </ThemeProvider>
// </ColorModeContext.Provider>
// );
//};