diff --git a/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs b/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs index 1568326..393870a 100644 --- a/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs +++ b/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs @@ -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) diff --git a/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs b/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs index 41b0618..3cd9c65 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs @@ -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); } } diff --git a/Surge365.MassEmailReact.Application/Interfaces/IUserRepository.cs b/Surge365.MassEmailReact.Application/Interfaces/IUserRepository.cs index 5a51f89..29e52b8 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IUserRepository.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IUserRepository.cs @@ -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 Authenticate(string refreshToken); Task GetByUsername(string username); Task GetByKey(int userKey); Task GetById(Guid userId); Task> GetAll(bool activeOnly = true); + + Task SaveRefreshToken(Guid userId, string refreshToken, string? previousToken = ""); } } diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/UserMap.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/UserMap.cs index 79620e8..90f34a3 100644 --- a/Surge365.MassEmailReact.Infrastructure/DapperMaps/UserMap.cs +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/UserMap.cs @@ -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"); diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/UserRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/UserRepository.cs index 290342e..bc5b3b3 100644 --- a/Surge365.MassEmailReact.Infrastructure/Repositories/UserRepository.cs +++ b/Surge365.MassEmailReact.Infrastructure/Repositories/UserRepository.cs @@ -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 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("@valid")) + return null; + + return await GetById(parameters.Get("@user_guid")); } public async Task GetByUsername(string username) @@ -110,5 +126,42 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories ); return users.AsList(); } + public async Task 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("@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(); + } + } } } \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs b/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs index faea461..7444fba 100644 --- a/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs +++ b/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs @@ -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); } } } diff --git a/Surge365.MassEmailReact.Web/src/components/auth/AuthCheck.tsx b/Surge365.MassEmailReact.Web/src/components/auth/AuthCheck.tsx index cf5113a..4a511eb 100644 --- a/Surge365.MassEmailReact.Web/src/components/auth/AuthCheck.tsx +++ b/Surge365.MassEmailReact.Web/src/components/auth/AuthCheck.tsx @@ -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'; diff --git a/Surge365.MassEmailReact.Web/src/components/auth/AuthContext.tsx b/Surge365.MassEmailReact.Web/src/components/auth/AuthContext.tsx index 716b1df..7adc049 100644 --- a/Surge365.MassEmailReact.Web/src/components/auth/AuthContext.tsx +++ b/Surge365.MassEmailReact.Web/src/components/auth/AuthContext.tsx @@ -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 it’s above everything + zIndex: 9999, // Ensure it’s above everything }} > = ({ children }) => { - return ( -
- {/* Header */} - - - {/* Sidebar */} - - - {/* Main Content Area */} -
- {children} -
- - {/* Footer */} -
-
Version 0.0.1
- - Copyright © 2025  - Surge365. - - All rights reserved. -
-
- ); -}; - -export default Layout; diff --git a/Surge365.MassEmailReact.Web/src/components/pages/App.tsx b/Surge365.MassEmailReact.Web/src/components/pages/App.tsx index ce91798..c583861 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/App.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/App.tsx @@ -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( () => ({ diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Home.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Home.tsx index 0668615..ec9a330 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/Home.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/Home.tsx @@ -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 = () => { - - Sent Mailing Statistics - ); diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppMain.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppMain.tsx deleted file mode 100644 index 3726cec..0000000 --- a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppMain.tsx +++ /dev/null @@ -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( - - - - ); -} else { - throw new Error('Root element not found'); -} diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppRouter.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppRouter.tsx deleted file mode 100644 index b58ce4e..0000000 --- a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppRouter.tsx +++ /dev/null @@ -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 ( - - - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - ); -}; - -export default App; diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Home.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Home.tsx deleted file mode 100644 index 8f2e6d6..0000000 --- a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Home.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; - -const Home: React.FC = () => { - return ( -
- {/* Content Header */} -
-
-
-
-

Dashboard

-
-
-
    -
  1. - Home -
  2. -
  3. - Dashboard -
  4. -
-
-
-
-
- - {/* Page-specific Content */} -
-
-
- {/* Example: Small Box Widget 1 */} -
-
-
-

150

-

New Orders

-
- - - - - More info - -
-
- {/* Additional small boxes, charts, or other widgets can be included here */} -
- {/* You can also add more rows for charts, direct chat, etc. */} -
-
-
- ); -}; - -export default Home; diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Login.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Login.tsx deleted file mode 100644 index b3b3a3a..0000000 --- a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Login.tsx +++ /dev/null @@ -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; -type FormErrors = Record; - -function Login() { - const [isLoading, setIsLoading] = useState(false); - const [spinners, setSpinnersState] = useState({}); - const [formErrors, setFormErrors] = useState({}); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false); - //const [user, setUser] = useState(null); - const [loginError, setLoginError] = useState(false); - const [loginErrorMessage, setLoginErrorMessage] = useState(''); - - //const setSpinners = (newValues: Partial) => { - // setSpinnersState((prevSpinners) => ({ - // ...prevSpinners, - // ...newValues, - // })); - //}; - const setSpinners = (newValues: Partial) => { - 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({ - 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 ( -
-
-

surge365 - React

-
-
-

Please sign in

-
- {loginError && ( - {loginErrorMessage ?? "Login error"} - )} - - Username - setUsername(e.target.value)} - required - autoFocus - size="sm" - /> - {spinners.Username && } - - - - Password - setPassword(e.target.value)} - required - size="sm" - /> - - - - -
-
- - -
- ); -} - -export default Login; diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Targets.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Targets.tsx deleted file mode 100644 index 11355fd..0000000 --- a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Targets.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; - -const Targets: React.FC = () => { - return ( -
- {/* Content Header */} -
-
-
-
-

Targets

-
-
-
    -
  1. - Home -
  2. -
  3. - Targets -
  4. -
-
-
-
-
- - {/* Page-specific Content */} -
-
-
- {/* Example: Small Box Widget 1 */} -
-
-
-

150

-

Targets

-
- - - - - More info - -
-
- {/* Additional small boxes, charts, or other widgets can be included here */} -
- {/* You can also add more rows for charts, direct chat, etc. */} -
-
-
- ); -}; - -export default Targets; diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Templates.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Templates.tsx deleted file mode 100644 index 1a6e480..0000000 --- a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Templates.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; - -const Templates: React.FC = () => { - return ( -
- {/* Content Header */} -
-
-
-
-

Templates

-
-
-
    -
  1. - Home -
  2. -
  3. - Templates -
  4. -
-
-
-
-
- - {/* Page-specific Content */} -
-
-
- {/* Example: Small Box Widget 1 */} -
-
-
-

150

-

Templates

-
- - - - - More info - -
-
- {/* Additional small boxes, charts, or other widgets can be included here */} -
- {/* You can also add more rows for charts, direct chat, etc. */} -
-
-
- ); -}; - -export default Templates; diff --git a/Surge365.MassEmailReact.Web/src/components/shared/eslint.config.js b/Surge365.MassEmailReact.Web/src/components/shared/eslint.config.js new file mode 100644 index 0000000..1e3db5b --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/shared/eslint.config.js @@ -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: {} + } +]; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/components/widgets/RecentMailingStatsChart.tsx b/Surge365.MassEmailReact.Web/src/components/widgets/RecentMailingStatsChart.tsx index e2a1e88..c47904c 100644 --- a/Surge365.MassEmailReact.Web/src/components/widgets/RecentMailingStatsChart.tsx +++ b/Surge365.MassEmailReact.Web/src/components/widgets/RecentMailingStatsChart.tsx @@ -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([]); 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 Loading...; } - if (!stats.length) { - return No data available; - } - // 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 ( + + + Sent Mailings - Last 14 Days + + ); } \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/theme/ThemeWrapper.tsx b/Surge365.MassEmailReact.Web/src/theme/ThemeWrapper.tsx new file mode 100644 index 0000000..ea5709e --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/theme/ThemeWrapper.tsx @@ -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 = ({ 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 ( +// +// +// +// {children} +// +// +// ); +//}; \ No newline at end of file