Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented admin login/signup #442

Merged
merged 3 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion backend/controller/admin.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,16 @@ async function loginAdmin(req, res) {
const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: "1h",
});
res.cookie("authToken", token, {
maxAge: 1000 * 60 * 60,
httpOnly: true,
secure: true,
});
Comment on lines +76 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance cookie security configuration

While the basic security settings are good, consider strengthening the cookie configuration:

Add sameSite and domain attributes to prevent CSRF attacks and restrict cookie scope:

 res.cookie("authToken", token, {
   maxAge: 1000 * 60 * 60,
   httpOnly: true,               
   secure: true,                
+  sameSite: 'strict',          // Prevent CSRF attacks
+  domain: process.env.COOKIE_DOMAIN, // Restrict to specific domain
 });

Committable suggestion was skipped due to low confidence.

res.json({
message: "Login successful",
token,
role: "admin",
admin: { id: admin._id, name: admin.name, email: admin.email },
admin: { id: admin._id, name: admin.name, email: admin.email, role: admin.role || "admin" },
});
} catch (error) {
logger.error("Error logging in admin:", {
Expand Down
1 change: 1 addition & 0 deletions backend/controller/customer.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ async function loginCustomer(req, res) {
id: customer._id,
name: customer.name,
email: customer.email,
role: "customer"
},
});
} catch (error) {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/Pages/Admin.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { message } from 'antd';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useUser } from '../../context/userContext';

const Admin = () => {
const [events, setEvents] = useState([]);
const [error, setError] = useState(null);
const navigate = useNavigate();
const {user} = useUser();

// Fetch all events
const fetchData = async () => {
Expand Down Expand Up @@ -122,7 +124,7 @@ const Admin = () => {
<div className="h-fit min-h-screen w-screen flex flex-col items-center justify-start p-12 pt-[10vh]">
<div className="Header w-full flex flex-col items-center">
<h1 className="title text-[#323232] font-black text-7xl mb-6">
Hi {Admin.name}!
Hi {user.name}!
</h1>
<h1 className="mt-[-2vh] text-[#666] font-semibold text-2xl">
Welcome to Admin Panel
Expand Down Expand Up @@ -265,7 +267,7 @@ const Admin = () => {
<div className="container grid grid-cols-1 gap-8 px-4 md:grid-cols-2 lg:grid-cols-1 md:px-6">
<div className="event-list">
{error && <p className="text-red-500">{error}</p>}
{events.map((event) => (
{events.length > 0 && events.map((event) => (
<div
Comment on lines +270 to 271
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance UI with loading and empty states.

The events rendering could be improved with better handling of different states.

Consider implementing these improvements:

+ const [isLoading, setIsLoading] = useState(true);

  const fetchData = async () => {
+   setIsLoading(true);
    try {
      const response = await fetch(...);
      const data = await response.json();
      setEvents(data);
    } catch (error) {
      setError(error.message);
    } finally {
+     setIsLoading(false);
    }
  };

- {events.length > 0 && events.map((event) => (
+ {isLoading ? (
+   <div className="flex justify-center items-center h-40">
+     <span className="loading loading-spinner loading-lg"></span>
+   </div>
+ ) : error ? (
+   <div className="text-red-500 text-center">
+     <p>Error loading events: {error}</p>
+     <button 
+       onClick={fetchData}
+       className="mt-4 bg-blue-500 text-white px-4 py-2 rounded"
+     >
+       Retry
+     </button>
+   </div>
+ ) : events.length === 0 ? (
+   <div className="text-center text-gray-500">
+     <p>No events found</p>
+   </div>
+ ) : (
+   events.map((event) => (

Committable suggestion skipped: line range outside the PR's diff.

key={event._id}
className="grid grid-cols-1 md:grid-cols-2 gap-10 lg:grid-cols-1 xl:grid-cols-2 md:px-6 lg:px-4 xl:px-0"
Expand Down
159 changes: 159 additions & 0 deletions frontend/src/components/Pages/Admin/AdminLogin.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React, { useState, useEffect } from 'react';
import photo from '../../../assets/login.png';
import { Link, useNavigate } from 'react-router-dom';
import { message } from 'antd';
import Cookies from 'js-cookie';
import { FaEye } from 'react-icons/fa';
import { FaEyeSlash } from 'react-icons/fa6';
import { useUser } from '../../../context/userContext';

const AdminLogin = () => {
const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using a more secure API URL configuration.

The current fallback to localhost could potentially cause issues in different environments.

Consider using a configuration object:

-const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000';
+const API_URL = import.meta.env.VITE_BACKEND_URL;
+if (!API_URL) {
+  throw new Error('Backend URL configuration is missing');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3000';
const API_URL = import.meta.env.VITE_BACKEND_URL;
if (!API_URL) {
throw new Error('Backend URL configuration is missing');
}

const [data, setData] = useState({
email: '',
password: '',
});
const [hidden, setHidden] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const {user, setUser} = useUser();

const navigate = useNavigate();

const handleChange = (e) => {
setData({ ...data, [e.target.name]: e.target.value });
};

const handleSubmit = async (e) => {
e.preventDefault();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
setError('Please enter a valid email address');
return;
}
if (data.password.length < 8) {
setError('Password must be at least 8 characters long');
return;
}
setIsLoading(true);
setError(null);
try {
const response = await fetch(`${API_URL}/api/admin/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
Comment on lines +41 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add CSRF protection to API requests.

The current implementation lacks CSRF protection for the login endpoint.

Consider adding CSRF token to the request headers:

 const response = await fetch(`${API_URL}/api/admin/login`, {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json',
+    'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
   },
   body: JSON.stringify(data),
 });

Committable suggestion was skipped due to low confidence.

const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Login failed');
}
Comment on lines +49 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling with more specific error messages.

The current error handling could be more informative by providing specific error messages based on different error scenarios.

 if (!response.ok) {
-  throw new Error(result.message || 'Login failed');
+  const errorMessage = {
+    401: 'Invalid email or password',
+    403: 'Account not verified',
+    429: 'Too many login attempts. Please try again later',
+  }[response.status] || result.message || 'Login failed';
+  throw new Error(errorMessage);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!response.ok) {
throw new Error(result.message || 'Login failed');
}
if (!response.ok) {
const errorMessage = {
401: 'Invalid email or password',
403: 'Account not verified',
429: 'Too many login attempts. Please try again later',
}[response.status] || result.message || 'Login failed';
throw new Error(errorMessage);
}

const res = JSON.stringify(result.admin)
Cookies.set('authToken', result.token, { expires: 1, secure: true });
Cookies.set("authenticatedUser", res, {expires: 1, secure: true})
Comment on lines +53 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance cookie security settings.

The current cookie configuration could be strengthened to better protect against XSS and CSRF attacks.

Apply these security enhancements:

-Cookies.set('authToken', result.token, { expires: 1, secure: true });
-Cookies.set("authenticatedUser", res, {expires: 1, secure: true})
+Cookies.set('authToken', result.token, {
+  expires: 1,
+  secure: true,
+  sameSite: 'strict',
+  path: '/'
+});
+Cookies.set("authenticatedUser", res, {
+  expires: 1,
+  secure: true,
+  sameSite: 'strict',
+  path: '/'
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Cookies.set('authToken', result.token, { expires: 1, secure: true });
Cookies.set("authenticatedUser", res, {expires: 1, secure: true})
Cookies.set('authToken', result.token, {
expires: 1,
secure: true,
sameSite: 'strict',
path: '/'
});
Cookies.set("authenticatedUser", res, {
expires: 1,
secure: true,
sameSite: 'strict',
path: '/'
});

setUser(result.admin)
message.success('Login successful');
navigate('/admin');
} catch (err) {
setError(err.message || 'An error occurred. Please try again.');
} finally {
setIsLoading(false);
}
};
Comment on lines +27 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement client-side rate limiting for login attempts.

To enhance security, consider implementing basic client-side rate limiting to complement server-side protection.

+const MAX_ATTEMPTS = 5;
+const LOCKOUT_TIME = 15 * 60 * 1000; // 15 minutes
+
 const AdminLogin = () => {
+  const [loginAttempts, setLoginAttempts] = useState(
+    parseInt(localStorage.getItem('loginAttempts') || '0')
+  );
+  const [lockoutUntil, setLockoutUntil] = useState(
+    parseInt(localStorage.getItem('lockoutUntil') || '0')
+  );
+
   const handleSubmit = async (e) => {
     e.preventDefault();
+    
+    const now = Date.now();
+    if (now < lockoutUntil) {
+      const minutesLeft = Math.ceil((lockoutUntil - now) / (60 * 1000));
+      setError(`Too many login attempts. Please try again in ${minutesLeft} minutes.`);
+      return;
+    }
+
     // ... existing validation code ...
+
     try {
       const response = await fetch(`${API_URL}/api/admin/login`, {
         // ... existing code ...
       });
       if (!response.ok) {
+        const newAttempts = loginAttempts + 1;
+        setLoginAttempts(newAttempts);
+        localStorage.setItem('loginAttempts', newAttempts.toString());
+        
+        if (newAttempts >= MAX_ATTEMPTS) {
+          const lockout = Date.now() + LOCKOUT_TIME;
+          setLockoutUntil(lockout);
+          localStorage.setItem('lockoutUntil', lockout.toString());
+          throw new Error(`Too many failed attempts. Please try again in 15 minutes.`);
+        }
         throw new Error(result.message || 'Login failed');
       }
+      // Reset on successful login
+      setLoginAttempts(0);
+      setLockoutUntil(0);
+      localStorage.removeItem('loginAttempts');
+      localStorage.removeItem('lockoutUntil');
       // ... existing success code ...
     } catch (err) {
       setError(err.message || 'An error occurred. Please try again.');
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSubmit = async (e) => {
e.preventDefault();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
setError('Please enter a valid email address');
return;
}
if (data.password.length < 8) {
setError('Password must be at least 8 characters long');
return;
}
setIsLoading(true);
setError(null);
try {
const response = await fetch(`${API_URL}/api/admin/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || 'Login failed');
}
const res = JSON.stringify(result.admin)
Cookies.set('authToken', result.token, { expires: 1, secure: true });
Cookies.set("authenticatedUser", res, {expires: 1, secure: true})
setUser(result.admin)
message.success('Login successful');
navigate('/admin');
} catch (err) {
setError(err.message || 'An error occurred. Please try again.');
} finally {
setIsLoading(false);
}
};
const MAX_ATTEMPTS = 5;
const LOCKOUT_TIME = 15 * 60 * 1000; // 15 minutes
const AdminLogin = () => {
const [loginAttempts, setLoginAttempts] = useState(
parseInt(localStorage.getItem('loginAttempts') || '0')
);
const [lockoutUntil, setLockoutUntil] = useState(
parseInt(localStorage.getItem('lockoutUntil') || '0')
);
const handleSubmit = async (e) => {
e.preventDefault();
const now = Date.now();
if (now < lockoutUntil) {
const minutesLeft = Math.ceil((lockoutUntil - now) / (60 * 1000));
setError(`Too many login attempts. Please try again in ${minutesLeft} minutes.`);
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
setError('Please enter a valid email address');
return;
}
if (data.password.length < 8) {
setError('Password must be at least 8 characters long');
return;
}
setIsLoading(true);
setError(null);
try {
const response = await fetch(`${API_URL}/api/admin/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const result = await response.json();
if (!response.ok) {
const newAttempts = loginAttempts + 1;
setLoginAttempts(newAttempts);
localStorage.setItem('loginAttempts', newAttempts.toString());
if (newAttempts >= MAX_ATTEMPTS) {
const lockout = Date.now() + LOCKOUT_TIME;
setLockoutUntil(lockout);
localStorage.setItem('lockoutUntil', lockout.toString());
throw new Error(`Too many failed attempts. Please try again in 15 minutes.`);
}
throw new Error(result.message || 'Login failed');
}
// Reset on successful login
setLoginAttempts(0);
setLockoutUntil(0);
localStorage.removeItem('loginAttempts');
localStorage.removeItem('lockoutUntil');
const res = JSON.stringify(result.admin)
Cookies.set('authToken', result.token, { expires: 1, secure: true });
Cookies.set("authenticatedUser", res, {expires: 1, secure: true})
setUser(result.admin)
message.success('Login successful');
navigate('/admin');
} catch (err) {
setError(err.message || 'An error occurred. Please try again.');
} finally {
setIsLoading(false);
}
};


useEffect(() => {
window.scrollTo(0, 0);
}, []);

return (
<div className="w-screen h-screen dark:bg-black flex items-center justify-center lg:pt-10 px-4">
{/* Background Image */}
<img
src={photo}
alt="login"
loading="lazy"
className="absolute w-3/4 lg:w-auto lg:opacity-100 opacity-10 object-cover"
/>
{/* Login Form */}
<form
onSubmit={handleSubmit}
className="z-10 p-8 lg:p-14 bg-[#f1e9dc] dark:bg-amber-800 dark:text-white flex flex-col gap-6 rounded-lg border-2 border-black shadow-[4px_4px_0px_0px_black] w-full max-w-md lg:max-w-xl"
>
<div className="text-[#323232] dark:text-white font-black text-4xl lg:text-6xl mb-2">
Admin Login,
<span className="block text-[#666] dark:text-gray-400 font-semibold text-lg lg:text-2xl mt-1">
Log in to continue
</span>
</div>

<input
className="w-full h-12 rounded-md border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[15px] font-semibold text-[#323232] p-2.5 focus:outline-none focus:border-[#2d8cf0] placeholder-[#666]"
name="email"
placeholder="Email"
type="email"
onChange={handleChange}
/>
Comment on lines +90 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance form accessibility.

The form inputs lack proper accessibility attributes.

Add necessary accessibility attributes:

+<label htmlFor="email" className="sr-only">Email</label>
 <input
+  id="email"
   className="w-full h-12 rounded-md border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[15px] font-semibold text-[#323232] p-2.5 focus:outline-none focus:border-[#2d8cf0] placeholder-[#666]"
   name="email"
   placeholder="Email"
   type="email"
+  aria-required="true"
+  aria-invalid={error ? "true" : "false"}
   onChange={handleChange}
 />

 <div className="relative w-full">
+  <label htmlFor="password" className="sr-only">Password</label>
   <input
+    id="password"
     className="w-full h-12 rounded-md border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[15px] font-semibold text-[#323232] p-2.5 focus:outline-none focus:border-[#2d8cf0] placeholder-[#666]"
     name="password"
     placeholder="Password"
     type={hidden ? 'password' : 'text'}
+    aria-required="true"
+    aria-invalid={error ? "true" : "false"}
     onChange={handleChange}
   />
   <button
     className="absolute top-1/2 transform -translate-y-1/2 right-4"
+    aria-label={hidden ? "Show password" : "Hide password"}
     onClick={(e) => {

Also applies to: 89-106


<div className="relative w-full">
<input
className="w-full h-12 rounded-md border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[15px] font-semibold text-[#323232] p-2.5 focus:outline-none focus:border-[#2d8cf0] placeholder-[#666]"
name="password"
placeholder="Password"
type={hidden ? 'password' : 'text'}
onChange={handleChange}
/>
<button
className="absolute top-1/2 transform -translate-y-1/2 right-4"
onClick={(e) => {
e.preventDefault();
setHidden(!hidden);
}}
>
{hidden ? <FaEyeSlash /> : <FaEye />}
</button>
</div>

<Link
to="/email-verify"
className="text-sm lg:text-base text-gray-500 dark:text-gray-200 hover:text-red-500 transition"
>
Forgot Password?
</Link>

<h3 className="flex justify-between items-center w-full text-sm lg:text-base">
Don’t have an account?
<Link
to="/admin-signup"
className="text-green-500 font-semibold hover:scale-110 transition"
>
Register Here
</Link>
</h3>

<Link
to={`${API_URL}/api/user/auth/google`}
className="w-full"
>
<button
type="button"
className="w-full h-12 rounded-md border-2 dark:text-white border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[17px] font-semibold text-[#323232] transition active:translate-x-[3px] active:translate-y-[3px]"
>
Sign in with Google
</button>
</Link>

{error && <p className="text-red-500 mt-2">{error}</p>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error message accessibility and visibility.

The error message display could be enhanced for better accessibility and user experience.

-{error && <p className="text-red-500 mt-2">{error}</p>}
+{error && (
+  <div
+    role="alert"
+    aria-live="polite"
+    className="p-3 bg-red-100 border border-red-400 text-red-700 rounded relative"
+  >
+    <span className="block sm:inline">{error}</span>
+  </div>
+)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{error && <p className="text-red-500 mt-2">{error}</p>}
{error && (
<div
role="alert"
aria-live="polite"
className="p-3 bg-red-100 border border-red-400 text-red-700 rounded relative"
>
<span className="block sm:inline">{error}</span>
</div>
)}


<button
type="submit"
className="w-full h-12 rounded-md dark:text-white border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[17px] font-semibold text-[#323232] transition active:translate-x-[3px] active:translate-y-[3px]"
>
{isLoading ? 'Loading...' : 'Let’s Log you in →'}
</button>
Comment on lines +148 to +153
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance loading state handling.

The current loading state implementation could be improved to prevent multiple form submissions and provide better visual feedback.

 <button
   type="submit"
+  disabled={isLoading}
   className="w-full h-12 rounded-md dark:text-white border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[17px] font-semibold text-[#323232] transition active:translate-x-[3px] active:translate-y-[3px]"
 >
-  {isLoading ? 'Loading...' : 'Let's Log you in →'}
+  {isLoading ? (
+    <span className="flex items-center justify-center">
+      <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
+        <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
+        <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
+      </svg>
+      Signing in...
+    </span>
+  ) : (
+    'Sign in →'
+  )}
 </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type="submit"
className="w-full h-12 rounded-md dark:text-white border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[17px] font-semibold text-[#323232] transition active:translate-x-[3px] active:translate-y-[3px]"
>
{isLoading ? 'Loading...' : 'Let’s Log you in →'}
</button>
<button
type="submit"
disabled={isLoading}
className="w-full h-12 rounded-md dark:text-white border-2 border-black bg-beige shadow-[4px_4px_0px_0px_black] text-[17px] font-semibold text-[#323232] transition active:translate-x-[3px] active:translate-y-[3px]"
>
{isLoading ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Signing in...
</span>
) : (
'Sign in →'
)}
</button>

</form>
</div>
);
};

export default AdminLogin;
Loading
Loading