diff --git a/.env b/.env
new file mode 100644
index 000000000..2a8da77da
--- /dev/null
+++ b/.env
@@ -0,0 +1,7 @@
+VITE_FIREBASE_API_KEY=AIzaSyDs0-gR5PrwDPRpyE6ZE0ua9sALYICwvcI
+VITE_FIREBASE_AUTH_DOMAIN=tarbiyah-sms.firebaseapp.com
+VITE_FIREBASE_PROJECT_ID=tarbiyah-sms
+VITE_FIREBASE_STORAGE_BUCKET=tarbiyah-sms.firebasestorage.app
+VITE_FIREBASE_MESSAGING_SENDER_ID=329966751488
+VITE_FIREBASE_APP_ID=1:329966751488:web:e4d4fd53397e7c21ccede5
+VITE_APP_FIREBASE_MEASUREMENT_ID=G-Y12T1TPW92
diff --git a/.env.example b/.env.example
new file mode 100644
index 000000000..2a8da77da
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,7 @@
+VITE_FIREBASE_API_KEY=AIzaSyDs0-gR5PrwDPRpyE6ZE0ua9sALYICwvcI
+VITE_FIREBASE_AUTH_DOMAIN=tarbiyah-sms.firebaseapp.com
+VITE_FIREBASE_PROJECT_ID=tarbiyah-sms
+VITE_FIREBASE_STORAGE_BUCKET=tarbiyah-sms.firebasestorage.app
+VITE_FIREBASE_MESSAGING_SENDER_ID=329966751488
+VITE_FIREBASE_APP_ID=1:329966751488:web:e4d4fd53397e7c21ccede5
+VITE_APP_FIREBASE_MEASUREMENT_ID=G-Y12T1TPW92
diff --git a/index.html b/index.html
index 9613ef3ee..ec89877d1 100644
--- a/index.html
+++ b/index.html
@@ -1,4 +1,5 @@
+<<<<<<< Updated upstream
+=======
+>>>>>>> Stashed changes
diff --git a/package.json b/package.json
index 7e992e447..32f3b0314 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,15 @@
"@popperjs/core": "^2.11.8",
"chart.js": "^4.4.7",
"classnames": "^2.5.1",
+<<<<<<< Updated upstream
"core-js": "^3.40.0",
+=======
+ "core-js": "^3.39.0",
+ "cors": "^2.8.5",
+ "express": "^4.21.2",
+ "firebase": "^11.2.0",
+ "firebase-admin": "^13.1.0",
+>>>>>>> Stashed changes
"prop-types": "^15.8.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/src/Actions/actions.js b/src/Actions/actions.js
new file mode 100644
index 000000000..e1b1340a3
--- /dev/null
+++ b/src/Actions/actions.js
@@ -0,0 +1,13 @@
+export const loginSuccess = (user) => ({
+ type: 'LOGIN_SUCCESS',
+ payload: user,
+ });
+
+ export const logout = () => ({
+ type: 'LOGOUT',
+ });
+
+ export const setLoading = (loading) => ({
+ type: 'SET_LOADING',
+ payload: loading,
+ });
\ No newline at end of file
diff --git a/src/App.js b/src/App.js
index f5b22393e..689061eab 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,59 +1,67 @@
-import React, { Suspense, useEffect } from 'react'
-import { HashRouter, Route, Routes } from 'react-router-dom'
-import { useSelector } from 'react-redux'
+import React, { Suspense, useEffect } from 'react';
+import { HashRouter, Route, Routes, Navigate } from 'react-router-dom';
+import { CSpinner, useColorModes } from '@coreui/react';
+import useAuth from './Firebase/useAuth';
+import './scss/style.scss';
+import './scss/examples.scss';
-import { CSpinner, useColorModes } from '@coreui/react'
-import './scss/style.scss'
-
-// We use those styles to show code examples, you should remove them in your application.
-import './scss/examples.scss'
-
-// Containers
-const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout'))
-
-// Pages
-const Login = React.lazy(() => import('./views/pages/login/Login'))
-const Register = React.lazy(() => import('./views/pages/register/Register'))
-const Page404 = React.lazy(() => import('./views/pages/page404/Page404'))
-const Page500 = React.lazy(() => import('./views/pages/page500/Page500'))
+const DefaultLayout = React.lazy(() => import('./layout/DefaultLayout'));
+const Login = React.lazy(() => import('./views/pages/login/Login'));
+const Register = React.lazy(() => import('./views/pages/register/Register'));
+const Page404 = React.lazy(() => import('./views/pages/page404/Page404'));
+const Page500 = React.lazy(() => import('./views/pages/page500/Page500'));
const App = () => {
- const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme')
- const storedTheme = useSelector((state) => state.theme)
+ const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme');
+ const { user, loading } = useAuth();
useEffect(() => {
- const urlParams = new URLSearchParams(window.location.href.split('?')[1])
- const theme = urlParams.get('theme') && urlParams.get('theme').match(/^[A-Za-z0-9\s]+/)[0]
- if (theme) {
- setColorMode(theme)
- }
+ const urlParams = new URLSearchParams(window.location.href.split('?')[1]);
+ const theme = urlParams.get('theme')?.match(/^[A-Za-z0-9\s]+/)?.[0];
+ if (theme) setColorMode(theme);
+ if (!isColorModeSet()) setColorMode('light');
+ }, [isColorModeSet, setColorMode]);
- if (isColorModeSet()) {
- return
- }
-
- setColorMode(storedTheme)
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
+ if (loading) {
+ return (
+
+
+
+ );
+ }
return (
-
-
-
- }
- >
+
+
+
+ }>
- } />
- } />
- } />
- } />
- } />
+ {/* Public routes */}
+ : }
+ />
+ } />
+ } />
+ } />
+
+ {/* Protected routes */}
+ : }
+ />
+
+ {/* Redirect unmatched routes */}
+ }
+ />
- )
-}
+ );
+};
-export default App
+export default App;
\ No newline at end of file
diff --git a/src/Firebase/firebase.js b/src/Firebase/firebase.js
new file mode 100644
index 000000000..54ade01c6
--- /dev/null
+++ b/src/Firebase/firebase.js
@@ -0,0 +1,19 @@
+import { initializeApp } from "firebase/app";
+import { getAnalytics } from "firebase/analytics";
+import { getAuth } from "firebase/auth";
+import { getFirestore } from "firebase/firestore";
+
+const firebaseConfig = {
+ apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
+ authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
+ projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
+ storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
+ appId: import.meta.env.VITE_FIREBASE_APP_ID,
+ };
+const app = initializeApp(firebaseConfig);
+const analytics = getAnalytics(app);
+const auth = getAuth(app);
+const firestore = getFirestore(app);
+
+export { app, analytics, auth, firestore };
diff --git a/src/Firebase/useAuth.js b/src/Firebase/useAuth.js
new file mode 100644
index 000000000..d5272f990
--- /dev/null
+++ b/src/Firebase/useAuth.js
@@ -0,0 +1,34 @@
+import { useEffect, useState } from 'react';
+import { onAuthStateChanged, signOut as firebaseSignOut } from 'firebase/auth';
+import { auth } from './firebase';
+
+const useAuth = () => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const unsubscribe = onAuthStateChanged(auth, (user) => {
+ if (user) {
+ setUser(user);
+ } else {
+ setUser(null);
+ }
+ setLoading(false);
+ });
+
+ return () => unsubscribe();
+ }, []);
+
+ const signOut = async () => {
+ try {
+ await firebaseSignOut(auth);
+ } catch (error) {
+ console.error("Error signing out:", error);
+ throw error;
+ }
+ };
+
+ return { user, loading, signOut };
+};
+
+export default useAuth;
\ No newline at end of file
diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js
index 30c0df82b..dced5906c 100644
--- a/src/components/header/AppHeaderDropdown.js
+++ b/src/components/header/AppHeaderDropdown.js
@@ -21,10 +21,23 @@ import {
cilUser,
} from '@coreui/icons'
import CIcon from '@coreui/icons-react'
-
+import { useNavigate } from 'react-router-dom';
+import useAuth from '../../Firebase/useAuth'
import avatar8 from './../../assets/images/avatars/8.jpg'
const AppHeaderDropdown = () => {
+ const { signOut } = useAuth();
+ const navigate = useNavigate();
+
+ const handleLogout = async () => {
+ try {
+ await signOut();
+ navigate('/login');
+ } catch (error) {
+ console.error('Logout error:', error);
+ }
+ };
+
return (
@@ -84,9 +97,9 @@ const AppHeaderDropdown = () => {
-
+
- Lock Account
+ Log Out
diff --git a/src/views/pages/login/Login.js b/src/views/pages/login/Login.js
index 1b2ee0baa..b507f8dff 100644
--- a/src/views/pages/login/Login.js
+++ b/src/views/pages/login/Login.js
@@ -1,5 +1,17 @@
-import React from 'react'
-import { Link } from 'react-router-dom'
+import React, { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import {
+ signInWithEmailAndPassword,
+ signInWithPopup,
+ GoogleAuthProvider
+} from 'firebase/auth';
+import {
+ getFirestore,
+ doc,
+ setDoc,
+ getDoc
+} from 'firebase/firestore';
+import { auth } from '../../../Firebase/firebase';
import {
CButton,
CCard,
@@ -12,11 +24,88 @@ import {
CInputGroup,
CInputGroupText,
CRow,
-} from '@coreui/react'
-import CIcon from '@coreui/icons-react'
-import { cilLockLocked, cilUser } from '@coreui/icons'
+ CAlert,
+} from '@coreui/react';
+import CIcon from '@coreui/icons-react';
+import { cilLockLocked, cilUser } from '@coreui/icons';
+
+// Configure Google Provider
+const googleProvider = new GoogleAuthProvider();
+googleProvider.addScope('https://www.googleapis.com/auth/forms.body');
+googleProvider.addScope('https://www.googleapis.com/auth/presentations');
+googleProvider.addScope('https://www.googleapis.com/auth/drive.file');
+
+const db = getFirestore();
const Login = () => {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ try {
+ await signInWithEmailAndPassword(auth, email, password);
+ navigate('/');
+ } catch (err) {
+ handleAuthError(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleGoogleLogin = async () => {
+ try {
+ const result = await signInWithPopup(auth, googleProvider);
+ const user = result.user;
+
+ // Check/create user document
+ const userRef = doc(db, 'users', user.uid);
+ const userDoc = await getDoc(userRef);
+
+ if (!userDoc.exists()) {
+ const [firstName, lastName] = user.displayName?.split(' ') || ['', ''];
+ await setDoc(userRef, {
+ firstName,
+ lastName,
+ email: user.email,
+ role: 'parent',
+ createdAt: serverTimestamp(),
+ lastLogin: serverTimestamp(),
+ loginCount: 0
+ });
+ }
+
+ navigate('/');
+ } catch (err) {
+ handleAuthError(err);
+ }
+ };
+
+ const handleAuthError = (error) => {
+ switch (error.code) {
+ case 'auth/user-not-found':
+ setError('No account found with this email');
+ break;
+ case 'auth/wrong-password':
+ setError('Incorrect password');
+ break;
+ case 'auth/popup-closed-by-user':
+ setError('Google sign-in window closed');
+ break;
+ case 'auth/account-exists-with-different-credential':
+ setError('Account exists with different login method');
+ break;
+ default:
+ setError('Login failed. Please try again.');
+ }
+ };
+
return (
@@ -25,15 +114,25 @@ const Login = () => {
-
+
Login
Sign In to your account
+ {error && {error}}
+
-
+ setEmail(e.target.value)}
+ required
+ />
+
@@ -42,12 +141,21 @@ const Login = () => {
type="password"
placeholder="Password"
autoComplete="current-password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ required
/>
+
-
- Login
+
+ {loading ? 'Loading...' : 'Login'}
@@ -56,17 +164,30 @@ const Login = () => {
+
+
+
+
+ Continue with Google
+
+
+ Google login creates a parent account by default
+
+
+
Sign up
-
- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
- tempor incididunt ut labore et dolore magna aliqua.
-
+
Don't have an account? Register now to access all features!
Register Now!
@@ -80,7 +201,7 @@ const Login = () => {
- )
-}
+ );
+};
-export default Login
+export default Login;
\ No newline at end of file
diff --git a/src/views/pages/login/authSlice.js b/src/views/pages/login/authSlice.js
new file mode 100644
index 000000000..213ae0d3f
--- /dev/null
+++ b/src/views/pages/login/authSlice.js
@@ -0,0 +1,27 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const initialState = {
+ user: null,
+ isAuthenticated: false,
+ loading: true
+};
+
+const authSlice = createSlice({
+ name: 'auth',
+ initialState,
+ reducers: {
+ loginSuccess: (state, action) => {
+ state.user = action.payload;
+ state.isAuthenticated = true;
+ state.loading = false;
+ },
+ logout: (state) => {
+ state.user = null;
+ state.isAuthenticated = false;
+ state.loading = false;
+ }
+ }
+});
+
+export const { loginSuccess, logout } = authSlice.actions;
+export default authSlice.reducer;
\ No newline at end of file
diff --git a/src/views/pages/register/Register.js b/src/views/pages/register/Register.js
index d78b24c8f..994fd64bf 100644
--- a/src/views/pages/register/Register.js
+++ b/src/views/pages/register/Register.js
@@ -1,4 +1,5 @@
-import React from 'react'
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import {
CButton,
CCard,
@@ -7,14 +8,396 @@ import {
CContainer,
CForm,
CFormInput,
+ CFormSelect,
CInputGroup,
CInputGroupText,
CRow,
-} from '@coreui/react'
-import CIcon from '@coreui/icons-react'
-import { cilLockLocked, cilUser } from '@coreui/icons'
+ CAlert,
+ CProgress
+} from '@coreui/react';
+import CIcon from '@coreui/icons-react';
+import { cilLockLocked, cilUser, cilPhone, cilContact, cilShieldAlt } from '@coreui/icons';
+import {
+ createUserWithEmailAndPassword,
+ signInWithPopup,
+ GoogleAuthProvider
+} from "firebase/auth";
+import { auth, firestore } from '../../../Firebase/firebase';
+import { doc, setDoc, serverTimestamp } from 'firebase/firestore';
+
+// Google Auth Provider Configuration
+const googleProvider = new GoogleAuthProvider();
const Register = () => {
+ const [step, setStep] = useState(1);
+ const [formData, setFormData] = useState({
+ email: '',
+ password: '',
+ confirmPassword: '',
+ firstName: '',
+ lastName: '',
+ phone: '',
+ role: '',
+ emergencyNumber: '',
+ subjects: '',
+ permissions: ''
+ });
+
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (step === 1) {
+ if (!validateStep1()) return;
+ setStep(2);
+ return;
+ }
+
+ if (step === 2) {
+ if (!validateStep2()) return;
+ setStep(3);
+ return;
+ }
+
+ await handleFinalSubmission();
+ };
+
+ const handleGoogleSignUp = async () => {
+ try {
+ const result = await signInWithPopup(auth, googleProvider);
+ const user = result.user;
+ const [firstName, lastName] = user.displayName?.split(' ') || ['', ''];
+
+ const userData = {
+ firstName,
+ lastName,
+ email: user.email,
+ phone: user.phoneNumber || '',
+ role: 'parent',
+ createdAt: serverTimestamp(),
+ updatedAt: serverTimestamp(),
+ lastLogin: serverTimestamp(),
+ loginCount: 0,
+ parentProfile: {
+ emergencyNumber: '',
+ children: [],
+ communicationPreferences: { email: true, sms: true }
+ }
+ };
+
+ await setDoc(doc(firestore, 'users', user.uid), userData);
+ navigate('/dashboard');
+ } catch (error) {
+ handleFirebaseError(error);
+ }
+ };
+
+ const validateStep1 = () => {
+ if (formData.password !== formData.confirmPassword) {
+ setError('Passwords do not match');
+ return false;
+ }
+ if (formData.password.length < 6) {
+ setError('Password must be at least 6 characters');
+ return false;
+ }
+ return true;
+ };
+
+ const validateStep2 = () => {
+ if (!formData.firstName || !formData.lastName || !formData.phone || !formData.role) {
+ setError('All fields are required');
+ return false;
+ }
+ return true;
+ };
+
+ const handleFinalSubmission = async () => {
+ setLoading(true);
+ try {
+ const userCredential = await createUserWithEmailAndPassword(
+ auth,
+ formData.email,
+ formData.password
+ );
+
+ const userData = {
+ firstName: formData.firstName,
+ lastName: formData.lastName,
+ email: formData.email,
+ phone: formData.phone,
+ role: formData.role,
+ createdAt: serverTimestamp(),
+ updatedAt: serverTimestamp(),
+ lastLogin: serverTimestamp(),
+ loginCount: 0,
+ ...getRoleSpecificData()
+ };
+
+ await setDoc(doc(firestore, 'users', userCredential.user.uid), userData);
+ navigate('/dashboard');
+ } catch (err) {
+ handleFirebaseError(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getRoleSpecificData = () => {
+ switch(formData.role) {
+ case 'parent':
+ return {
+ parentProfile: {
+ emergencyNumber: formData.emergencyNumber,
+ children: [],
+ communicationPreferences: { email: true, sms: true }
+ }
+ };
+ case 'teacher':
+ return {
+ teacherProfile: {
+ subjects: formData.subjects.split(',').map(s => s.trim()),
+ classesManaged: [],
+ teacherPerformance: { averageClassAttendance: 0, feedbackScore: 0 }
+ }
+ };
+ case 'admin':
+ return {
+ adminProfile: {
+ permissions: formData.permissions.split(',').map(p => p.trim()),
+ auditMetrics: { lastAudit: null, actionsLogged: 0 }
+ }
+ };
+ default:
+ return {};
+ }
+ };
+
+ const handleChange = (field, value) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ };
+
+ const handleFirebaseError = (error) => {
+ switch (error.code) {
+ case 'auth/email-already-in-use':
+ setError('Email is already registered');
+ break;
+ case 'auth/invalid-email':
+ setError('Invalid email address');
+ break;
+ case 'auth/weak-password':
+ setError('Password must be at least 6 characters');
+ break;
+ case 'auth/popup-closed-by-user':
+ setError('Google sign-up window was closed');
+ break;
+ case 'auth/account-exists-with-different-credential':
+ setError('Email already exists with different login method');
+ break;
+ default:
+ setError('Registration failed. Please try again.');
+ }
+ };
+
+ const renderStep = () => {
+ switch (step) {
+ case 1:
+ return (
+ <>
+
+
+
+
+ handleChange('email', e.target.value)}
+ required
+ />
+
+
+ Use your official email address
+
+
+
+
+
+
+ handleChange('password', e.target.value)}
+ required
+ />
+
+
+ Minimum 6 characters with letters and numbers
+
+
+
+
+
+
+ handleChange('confirmPassword', e.target.value)}
+ required
+ />
+
+
+
+
+
+ Sign up with Google
+
+
+ Google sign-up defaults to Parent role
+
+
+ >
+ );
+
+ case 2:
+ return (
+ <>
+
+
+
+
+ handleChange('firstName', e.target.value)}
+ required
+ />
+
+
+ Legal name as per official documents
+
+
+
+
+
+
+ handleChange('lastName', e.target.value)}
+ required
+ />
+
+
+
+
+
+
+ handleChange('phone', e.target.value)}
+ required
+ />
+
+
+ Include country code for SMS notifications
+
+
+
+
+
+
+ handleChange('role', e.target.value)}
+ required
+ >
+
+
+
+
+
+
+
+ Choose your primary role in the institution
+
+ >
+ );
+
+ case 3:
+ return (
+ <>
+ {formData.role === 'parent' && (
+ <>
+
+
+
+
+ handleChange('emergencyNumber', e.target.value)}
+ required
+ />
+
+
+ Primary emergency contact number
+
+ >
+ )}
+
+ {formData.role === 'teacher' && (
+ <>
+
+
+
+
+ handleChange('subjects', e.target.value)}
+ required
+ />
+
+
+ Example: Mathematics, Physics, Chemistry
+
+ >
+ )}
+
+ {formData.role === 'admin' && (
+ <>
+
+
+
+
+ handleChange('permissions', e.target.value)}
+ required
+ />
+
+
+ Example: user_management, system_config
+
+ >
+ )}
+ >
+ );
+ }
+ };
+
return (
@@ -22,41 +405,33 @@ const Register = () => {
-
- Register
- Create your account
-
-
-
-
-
-
-
- @
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
Register ({step}/3)
+
+ {step === 1 && 'Account Setup'}
+ {step === 2 && 'Personal Information'}
+ {step === 3 && `${formData.role.charAt(0).toUpperCase() + formData.role.slice(1)} Details`}
+
+
+
+ {error && {error}}
+
+ {renderStep()}
+
- Create Account
+
+ {loading ? 'Processing...' : step === 3 ? 'Complete Registration' : 'Next Step'}
+
@@ -65,7 +440,7 @@ const Register = () => {
- )
-}
+ );
+};
-export default Register
+export default Register;
\ No newline at end of file