diff --git a/backend/rewire/admin/settings.py b/backend/rewire/admin/settings.py index 52d3a15..9a7e71f 100644 --- a/backend/rewire/admin/settings.py +++ b/backend/rewire/admin/settings.py @@ -49,6 +49,7 @@ 'rest_framework', 'core', 'rest_framework_simplejwt', + 'corsheaders' ] @@ -60,6 +61,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware' ] ROOT_URLCONF = 'admin.urls' @@ -146,7 +148,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -# Email settings +# Email settings EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT = 587 @@ -160,3 +162,37 @@ 'rest_framework.authentication.SessionAuthentication', ] } + +# CORS Settings +CORS_ALLOW_ALL_ORIGINS = False + +CORS_ALLOWED_ORIGINS = [ + "http://localhost:8081", + "http://127.0.0.1:8081", + "http://localhost:8000", + "http://127.0.0.1:8000", +] + +CORS_ALLOW_CREDENTIALS = True + + +CORS_ALLOW_METHODS = [ + "DELETE", + "GET", + "OPTIONS", + "PATCH", + "POST", + "PUT", +] + +CORS_ALLOW_HEADERS = [ + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-csrftoken", + "x-requested-with", +] diff --git a/backend/rewire/admin/urls.py b/backend/rewire/admin/urls.py index 2e9432c..967211f 100644 --- a/backend/rewire/admin/urls.py +++ b/backend/rewire/admin/urls.py @@ -31,8 +31,8 @@ path('login', login_user), path('delete-user', delete_user), path('update-user', update_user), - path('forget-password', forget_password), - path('reset-password', reset_password), + path('forget-password/', forget_password), + path('reset-password///', reset_password, name='password_reset_confirm'), path('questionnaire/', include('questionnaire.urls')), path('recommendations/', include('recommendations.urls')), path('aiprofile/', include('aiprofile.urls')), diff --git a/backend/rewire/admin/views.py b/backend/rewire/admin/views.py index 3265895..a74c47d 100644 --- a/backend/rewire/admin/views.py +++ b/backend/rewire/admin/views.py @@ -41,21 +41,35 @@ def signup_step_two(request): if request.method == 'POST': try: data = json.loads(request.body) - email = data['email'] - password = data['password'] - confirm_password = data['confirm_password'] + + # Get all fields from the request + first_name = data.get('first_name') + last_name = data.get('last_name') + user_name = data.get('user_name') + email = data.get('email') + password = data.get('password') + confirm_password = data.get('confirm_password') + + # Validate all required fields + if not all([first_name, last_name, user_name, email, password, confirm_password]): + return JsonResponse({'error': 'All fields are required'}, status=400) if password != confirm_password: return JsonResponse({'error': 'Passwords do not match'}, status=400) - signup_data = request.session.get('signup_data') - if not signup_data: - return JsonResponse({'error': 'No data from step one'}, status=400) + # Check if username exists + if User.objects.filter(user_name=user_name).exists(): + return JsonResponse({'error': 'Username already exists'}, status=400) + + # Check if email exists + if User.objects.filter(email=email).exists(): + return JsonResponse({'error': 'Email already exists'}, status=400) + # Create user user = User.objects.create( - first_name=signup_data['first_name'], - last_name=signup_data['last_name'], - user_name=signup_data['user_name'], + first_name=first_name, + last_name=last_name, + user_name=user_name, email=email, password=make_password(password) ) @@ -63,9 +77,15 @@ def signup_step_two(request): user.save() return JsonResponse({'message': 'User created successfully'}, status=201) + except KeyError as e: print(e) - return JsonResponse({'error': 'Invalid data'}, status=400) + return JsonResponse({'error': f'Missing field: {str(e)}'}, status=400) + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON format'}, status=400) + except Exception as e: + print(f"Error in signup step two: {str(e)}") + return JsonResponse({'error': str(e)}, status=500) else: return JsonResponse({'error': 'Invalid HTTP method'}, status=405) @@ -153,7 +173,8 @@ def forget_password(request): user = User.objects.filter(email=email).first() token = default_token_generator.make_token(user) uid = urlsafe_base64_encode(force_bytes(user.pk)) - reset_link = f"http://localhost:8000/reset_password/{uid}/{token}/" + + reset_link = f"http://localhost:8081/reset_password/{uid}/{token}/" send_mail( 'Reset your password for rewire', @@ -173,8 +194,14 @@ def forget_password(request): @api_view(['POST']) @csrf_exempt -def reset_password(request, uidb64, token): - if request.method == 'POST': +def reset_password(request, uidb64=None, token=None): + # Now the function can handle both the form submission and the link validation + if request.method == 'GET': + # This is when the user clicks the link + # You can return a response or redirect to frontend + return JsonResponse({'valid': True, 'uidb64': uidb64, 'token': token}) + elif request.method == 'POST': + # Original password reset logic try: data = json.loads(request.body) new_password = data['new_password'] diff --git a/backend/rewire/requirements.txt b/backend/rewire/requirements.txt index fb97d17..4f22644 100644 --- a/backend/rewire/requirements.txt +++ b/backend/rewire/requirements.txt @@ -14,6 +14,7 @@ cryptography==44.0.2 daphne==4.1.2 distro==1.9.0 Django==5.1.6 +django-cors-headers==4.2.0 djangorestframework==3.15.2 djangorestframework_simplejwt==5.4.0 h11==0.14.0 diff --git a/frontend/App.js b/frontend/App.js index 429ea23..7329c6c 100644 --- a/frontend/App.js +++ b/frontend/App.js @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { Icon } from './app/fragments/icon'; import { useLogin } from './app/hooks/login-service'; import { NavigationContainer } from '@react-navigation/native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { ThemeProvider, useThemeToggle } from './app/hooks/theme-service'; -import { SafeAreaView, Platform, StatusBar } from 'react-native'; +import { SafeAreaView, Platform, StatusBar, Linking } from 'react-native'; import Home from './app/screens/tabs/Home'; import Login from './app/screens/Login'; @@ -16,23 +16,88 @@ import RebotWelcome from './app/screens/RebotWelcome'; import SignupStepOne from './app/screens/signupStepOne'; import SignupStepTwo from './app/screens/signupStepTwo'; import ForgotPassword from './app/screens/ForgotPassword'; +import ResetPassword from './app/screens/ResetPassword'; import RebotChatInterface from './app/screens/RebotChatInterface'; import Settings from './app/screens/Settings'; import About from './app/screens/About'; import PrivacyPolicy from './app/screens/PrivacyPolicy'; import TermsAndConditions from './app/screens/TermsAndConditions'; +import { AuthProvider } from './app/hooks/login-service'; export default function App() { return ( - - - + + + + + ); } function AppContent() { const { loggedIn } = useLogin(); const { theme } = useThemeToggle(); + const navigationRef = useRef(null); + + // Configuration for linking + const linking = { + prefixes: ['rewire://', 'https://rewire.app', 'http://localhost:8000'], + config: { + screens: { + ResetPassword: { + path: 'reset_password/:uidb64/:token', + parse: { + uidb64: (uidb64) => uidb64, + token: (token) => token, + }, + }, + }, + }, + }; + + // Handle deep links manually for more control (optional) + useEffect(() => { + // Function to handle incoming links + const handleDeepLink = ({ url }) => { + if (!url) return; + + // Parse the URL to extract the path and parameters + try { + const parsedUrl = new URL(url); + const pathSegments = parsedUrl.pathname.split('/').filter(Boolean); + + // Check if this is a reset password link + if (pathSegments.length >= 3 && pathSegments[0] === 'reset_password') { + const uidb64 = pathSegments[1]; + const token = pathSegments[2]; + + // Navigate to the ResetPassword screen with extracted parameters + if (navigationRef.current) { + navigationRef.current.navigate('ResetPassword', { uidb64, token }); + } + } + } catch (error) { + console.error('Error handling deep link:', error); + } + }; + + // Listen for incoming links when the app is already running + const subscription = Linking.addEventListener('url', handleDeepLink); + + // Check for any initial URL used to open the app + Linking.getInitialURL().then(url => { + if (url) { + handleDeepLink({ url }); + } + }).catch(err => { + console.error('Error getting initial URL:', err); + }); + + // Clean up the event listener on unmount + return () => { + subscription.remove(); + }; + }, []); return ( - + {loggedIn ? : } @@ -101,6 +170,7 @@ function LoginNavigator() { + diff --git a/frontend/app/app.settings.ts b/frontend/app/app.settings.ts index 56e2d17..72f2019 100644 --- a/frontend/app/app.settings.ts +++ b/frontend/app/app.settings.ts @@ -1,13 +1,3 @@ -/** - * App Configuration - * ----------------- - * This file contains all development-level app configurations. - * Not to be confused with user-configurable app settings like theme preferences. - * - * Note: - * To connect to a local backend instead of production: Set isProduction to false - */ - const isProduction = false; const DOMAINS = { @@ -16,26 +6,31 @@ const DOMAINS = { }; const PORTS = { - production: 8000, + production: 8081, local: 8000 }; const BACKEND_DOMAIN = isProduction ? DOMAINS.production : DOMAINS.local; const BACKEND_PORT = isProduction ? PORTS.production : PORTS.local; -const BACKEND_URL = `${BACKEND_DOMAIN}:${BACKEND_PORT}`; - -interface Setting { - isUserVisible: boolean; - value: string | number | object; -} +const BACKEND_URL = `http://${BACKEND_DOMAIN}:${BACKEND_PORT}`; -interface Settings { - [key: string]: Setting; -} - -export const settings: Settings = { +export const settings = { rebotWebsocket: { isUserVisible: false, value: `ws://${BACKEND_URL}/ws/rebot` }, -}; + apiBaseUrl: { + isUserVisible: false, + value: BACKEND_URL + }, + authEndpoints: { + isUserVisible: false, + value: { + login: "/login", + signupStepOne: "/signup/step-one", + signupStepTwo: "/signup/step-two", + forgotPassword: "/forget-password/", + resetPassword: "/reset-password" + } + } +}; \ No newline at end of file diff --git a/frontend/app/assets/eye-closed.png b/frontend/app/assets/eye-closed.png new file mode 100644 index 0000000..fb2e970 Binary files /dev/null and b/frontend/app/assets/eye-closed.png differ diff --git a/frontend/app/assets/eye-open.png b/frontend/app/assets/eye-open.png new file mode 100644 index 0000000..eff78ac Binary files /dev/null and b/frontend/app/assets/eye-open.png differ diff --git a/frontend/app/hooks/login-service.js b/frontend/app/hooks/login-service.js index 77fc296..e4e1d6a 100644 --- a/frontend/app/hooks/login-service.js +++ b/frontend/app/hooks/login-service.js @@ -1,6 +1,63 @@ -import { useState } from "react"; +import { useState, useEffect, createContext, useContext } from "react"; +import { authService, storeToken, getToken, removeToken } from "../services/auth-service"; -export function useLogin() { - const [loggedIn, setLoggedIn] = useState(true); - return { loggedIn, setLoggedIn }; -} \ No newline at end of file +const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [loggedIn, setLoggedIn] = useState(false); + const [loading, setLoading] = useState(true); + const [user, setUser] = useState(null); + + useEffect(() => { + // Check if user is logged in on app start + const checkLoginStatus = async () => { + const token = await getToken(); + if (token) { + setLoggedIn(true); + // You could fetch user profile here if needed + } + setLoading(false); + }; + + checkLoginStatus(); + }, []); + + const login = async (email, password) => { + try { + const response = await authService.login(email, password); + + if (response.message === 'Login successful') { + // For JWT, the token would typically be in the response + if (response.token) { + await storeToken(response.token); + } + setLoggedIn(true); + return true; + } + return false; + } catch (error) { + console.error("Login failed:", error); + return false; + } + }; + + const logout = async () => { + await removeToken(); + setLoggedIn(false); + setUser(null); + }; + + return ( + + {children} + + ); +}; + +export const useLogin = () => useContext(AuthContext); \ No newline at end of file diff --git a/frontend/app/screens/ForgotPassword.js b/frontend/app/screens/ForgotPassword.js index ceb5f7a..0396b61 100644 --- a/frontend/app/screens/ForgotPassword.js +++ b/frontend/app/screens/ForgotPassword.js @@ -6,18 +6,41 @@ import { TouchableOpacity, StyleSheet, Image, + Alert, + ActivityIndicator } from 'react-native'; +import { authService } from '../services/auth-service'; const ForgotPassword = ({ navigation }) => { const [email, setEmail] = useState(''); + const [loading, setLoading] = useState(false); const handleResetPassword = async () => { + // Validate email + if (!email.trim()) { + Alert.alert('Error', 'Please enter your email address'); + return; + } + + // Email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + Alert.alert('Error', 'Please enter a valid email address'); + return; + } + + setLoading(true); try { - // Implement password reset logic here - console.log('Password reset requested for:', email); - // You can add your password reset API call here + const response = await authService.forgotPassword(email); + Alert.alert( + 'Success', + 'Password reset link has been sent to your email', + [{ text: 'OK', onPress: () => navigation.navigate('Login') }] + ); } catch (error) { - console.error('Password reset failed:', error); + Alert.alert('Error', error.message || 'Password reset failed. Please try again.'); + } finally { + setLoading(false); } }; @@ -48,20 +71,27 @@ const ForgotPassword = ({ navigation }) => { keyboardType="email-address" autoCapitalize="none" placeholderTextColor="#A9A9A9" + editable={!loading} /> {/* Reset button */} - REQUEST RESET LINK + {loading ? ( + + ) : ( + REQUEST RESET LINK + )} {/* Back to login link */} navigation.navigate('Login')} style={styles.backToLoginContainer} + disabled={loading} > Back To Login @@ -114,6 +144,10 @@ const styles = StyleSheet.create({ alignItems: 'center', marginBottom: 20, }, + resetButtonDisabled: { + backgroundColor: '#A0A0A0', + opacity: 0.7, + }, resetButtonText: { color: '#fff', fontSize: 16, diff --git a/frontend/app/screens/Login.js b/frontend/app/screens/Login.js index 60423f2..63cf98b 100644 --- a/frontend/app/screens/Login.js +++ b/frontend/app/screens/Login.js @@ -12,15 +12,25 @@ import { useLogin } from '../hooks/login-service'; const Login = ({ navigation }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); const { login } = useLogin(); const handleLogin = async () => { + if (!email || !password) { + console.log('Error', 'Please enter both email and password'); + return; + } + + setLoading(true); try { - await login(email, password); - // Navigation will be handled by App.js based on login state + const success = await login(email, password); + if (!success) { + console.log('Login Failed', 'Invalid email or password'); + } } catch (error) { - // Handle login error - console.error('Login failed:', error); + Alert.alert('Error', error.message || 'Login failed. Please try again.'); + } finally { + setLoading(false); } }; diff --git a/frontend/app/screens/ResetPassword.js b/frontend/app/screens/ResetPassword.js new file mode 100644 index 0000000..9a52937 --- /dev/null +++ b/frontend/app/screens/ResetPassword.js @@ -0,0 +1,230 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + Image, + Alert, + ActivityIndicator +} from 'react-native'; +import { authService } from '../services/auth-service'; + +const ResetPassword = ({ navigation, route }) => { + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [loading, setLoading] = useState(false); + + // Get token and uid from route params if provided + const { uidb64, token } = route.params || {}; + + // Validate that we have the required parameters + useEffect(() => { + if (!uidb64 || !token) { + Alert.alert( + 'Invalid Link', + 'The password reset link is invalid or has expired.', + [{ text: 'Go to Login', onPress: () => navigation.navigate('Login') }] + ); + } + }, [uidb64, token]); + + const handleResetPassword = async () => { + // Validate password fields + if (!password.trim()) { + Alert.alert('Error', 'Please enter your new password'); + return; + } + + if (!confirmPassword.trim()) { + Alert.alert('Error', 'Please confirm your password'); + return; + } + + if (password !== confirmPassword) { + Alert.alert('Error', 'Passwords do not match'); + return; + } + + setLoading(true); + try { + const response = await authService.resetPassword(uidb64, token, password); + + Alert.alert( + 'Success', + 'Your password has been reset successfully.', + [{ text: 'Login Now', onPress: () => navigation.navigate('Login') }] + ); + } catch (error) { + Alert.alert('Error', error.message || 'Password reset failed. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( + + Reset Password + + {/* Illustration */} + + + + + + Please create and enter a new password for your account security. + + + {/* Password input */} + + + setShowPassword(!showPassword)} + > + + + + + {/* Confirm Password input */} + + + setShowConfirmPassword(!showConfirmPassword)} + > + + + + + {/* Submit button */} + + {loading ? ( + + ) : ( + SUBMIT + )} + + + {/* Back to login link */} + navigation.navigate('Login')} + style={styles.backToLoginContainer} + disabled={loading} + > + Back To Login + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + padding: 20, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#16837D', + marginTop: 40, + }, + imageContainer: { + alignItems: 'center', + marginVertical: 40, + }, + image: { + width: '80%', + height: 200, + }, + instructionText: { + fontSize: 16, + color: '#333', + marginBottom: 20, + lineHeight: 24, + }, + inputContainer: { + position: 'relative', + marginBottom: 20, + }, + input: { + width: '100%', + height: 50, + borderWidth: 1, + borderColor: '#E8E8E8', + borderRadius: 8, + paddingHorizontal: 15, + backgroundColor: '#F8F8F8', + paddingRight: 50, // Space for the eye icon + }, + eyeIcon: { + position: 'absolute', + right: 15, + top: 12, + }, + icon: { + width: 24, + height: 24, + }, + submitButton: { + width: '100%', + height: 50, + backgroundColor: '#16837D', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }, + submitButtonDisabled: { + backgroundColor: '#A0A0A0', + opacity: 0.7, + }, + submitButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: 'bold', + }, + backToLoginContainer: { + alignItems: 'center', + }, + backToLoginText: { + color: '#16837D', + fontSize: 16, + }, +}); + +export default ResetPassword; \ No newline at end of file diff --git a/frontend/app/screens/signupStepOne.js b/frontend/app/screens/signupStepOne.js index 0caa88b..347ad51 100644 --- a/frontend/app/screens/signupStepOne.js +++ b/frontend/app/screens/signupStepOne.js @@ -7,24 +7,38 @@ import { StyleSheet, Image, SafeAreaView, + ActivityIndicator, + Alert, } from 'react-native'; +import { authService } from '../services/auth-service'; const SignupStepOne = ({ navigation }) => { const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [username, setUsername] = useState(''); + const [loading, setLoading] = useState(false); - const handleNext = () => { + const handleNext = async () => { if (!firstName.trim() || !lastName.trim() || !username.trim()) { + Alert.alert('Error', 'Please fill in all fields'); return; } - // Navigate to step two with user data - navigation.navigate('SignupStepTwo', { - firstName, - lastName, - username - }); + setLoading(true); + try { + const response = await authService.signupStepOne(firstName, lastName, username); + + // If successful, navigate to step two with the user data + navigation.navigate('SignupStepTwo', { + firstName, + lastName, + username + }); + } catch (error) { + Alert.alert('Error', error.message || 'Signup failed. Please try again.'); + } finally { + setLoading(false); + } }; return ( @@ -48,6 +62,7 @@ const SignupStepOne = ({ navigation }) => { value={firstName} onChangeText={setFirstName} placeholderTextColor="#B8B8B8" + editable={!loading} /> { value={lastName} onChangeText={setLastName} placeholderTextColor="#B8B8B8" + editable={!loading} /> { value={username} onChangeText={setUsername} placeholderTextColor="#B8B8B8" + editable={!loading} /> @@ -75,10 +92,15 @@ const SignupStepOne = ({ navigation }) => { {/* Next Button */} navigation.navigate('SignupStepTwo')} + style={[styles.nextButton, loading && styles.nextButtonDisabled]} + onPress={handleNext} + disabled={loading} > - Next + {loading ? ( + + ) : ( + Next + )} {/* Sign In Link */} @@ -157,6 +179,10 @@ const styles = StyleSheet.create({ alignItems: 'center', marginBottom: 15, }, + nextButtonDisabled: { + backgroundColor: '#A0A0A0', + opacity: 0.7, + }, nextButtonText: { color: '#fff', fontSize: 18, diff --git a/frontend/app/screens/signupStepTwo.js b/frontend/app/screens/signupStepTwo.js index 9a35e9b..bdca4b5 100644 --- a/frontend/app/screens/signupStepTwo.js +++ b/frontend/app/screens/signupStepTwo.js @@ -7,8 +7,11 @@ import { StyleSheet, Image, SafeAreaView, - CheckBox, + ActivityIndicator, + Alert, } from 'react-native'; +import { authService } from '../services/auth-service'; +import { useLogin } from '../hooks/login-service'; const SignupStepTwo = ({ navigation, route }) => { const { firstName, lastName, username } = route.params || {}; @@ -17,35 +20,56 @@ const SignupStepTwo = ({ navigation, route }) => { const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [agreeToTerms, setAgreeToTerms] = useState(false); + const [loading, setLoading] = useState(false); + const { login } = useLogin(); const handleSignup = async () => { + // Validate inputs if (!email.trim() || !password.trim() || !confirmPassword.trim()) { + Alert.alert('Error', 'Please fill in all fields'); return; } if (password !== confirmPassword) { + Alert.alert('Error', 'Passwords do not match'); return; } if (!agreeToTerms) { + Alert.alert('Error', 'Please agree to the terms and conditions'); return; } - // Combine data from both steps - const userData = { - firstName, - lastName, - username, - email, - password, - }; - + setLoading(true); try { - - console.log('User registration data:', userData); + + await authService.signupStepTwo(firstName, lastName, username, email, password, confirmPassword); + // Show success message + Alert.alert( + 'Success', + 'Account created successfully!', + [ + { + text: 'Login Now', + onPress: async () => { + try { + // Attempt to log in the user automatically + const success = await login(email, password); + if (!success) { + navigation.navigate('Login'); + } + } catch (error) { + navigation.navigate('Login'); + } + } + } + ] + ); } catch (error) { - console.error('Signup failed:', error); + Alert.alert('Error', error.message || 'Signup failed. Please try again.'); + } finally { + setLoading(false); } }; @@ -72,6 +96,7 @@ const SignupStepTwo = ({ navigation, route }) => { keyboardType="email-address" autoCapitalize="none" placeholderTextColor="#B8B8B8" + editable={!loading} /> { onChangeText={setPassword} secureTextEntry placeholderTextColor="#B8B8B8" + editable={!loading} /> { onChangeText={setConfirmPassword} secureTextEntry placeholderTextColor="#B8B8B8" + editable={!loading} /> @@ -98,6 +125,7 @@ const SignupStepTwo = ({ navigation, route }) => { setAgreeToTerms(!agreeToTerms)} + disabled={loading} > { By creating an account your agree to our{' '} - Term and Conditions + navigation.navigate('TermsAndConditions')} + > + Terms and Conditions + @@ -119,10 +152,15 @@ const SignupStepTwo = ({ navigation, route }) => { {/* Signup Button */} - Signup + {loading ? ( + + ) : ( + Signup + )} {/* Sign In Link */} @@ -231,6 +269,10 @@ const styles = StyleSheet.create({ alignItems: 'center', marginBottom: 15, }, + signupButtonDisabled: { + backgroundColor: '#A0A0A0', + opacity: 0.7, + }, signupButtonText: { color: '#fff', fontSize: 18, diff --git a/frontend/app/services/auth-service.js b/frontend/app/services/auth-service.js new file mode 100644 index 0000000..e5c8661 --- /dev/null +++ b/frontend/app/services/auth-service.js @@ -0,0 +1,149 @@ +import { settings } from '../app.settings'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const API_URL = settings.apiBaseUrl.value; +const AUTH_ENDPOINTS = settings.authEndpoints.value; + +export const storeToken = async (token) => { + try { + await AsyncStorage.setItem('auth_token', token); + } catch (error) { + console.error('Error storing token:', error); + } +}; + +export const getToken = async () => { + try { + return await AsyncStorage.getItem('auth_token'); + } catch (error) { + console.error('Error getting token:', error); + return null; + } +}; + +export const removeToken = async () => { + try { + await AsyncStorage.removeItem('auth_token'); + } catch (error) { + console.error('Error removing token:', error); + } +}; + +export const authService = { + login: async (email, password) => { + try { + const response = await fetch(`${API_URL}${AUTH_ENDPOINTS.login}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Login failed'); + } + + return data; + } catch (error) { + console.error('Login error:', error); + throw error; + } + }, + + signupStepOne: async (firstName, lastName, userName) => { + try { + const response = await fetch(`${API_URL}${AUTH_ENDPOINTS.signupStepOne}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + first_name: firstName, + last_name: lastName, + user_name: userName + }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Signup step one failed'); + } + + return data; + } catch (error) { + console.error('Signup step one error:', error); + throw error; + } + }, + + signupStepTwo: async (firstName, lastName, userName, email, password, confirmPassword) => { + try { + const response = await fetch(`${API_URL}${AUTH_ENDPOINTS.signupStepTwo}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + first_name : firstName, + last_name : lastName, + user_name : userName, + email, + password, + confirm_password: confirmPassword + }), + credentials: 'include' + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Signup step two failed'); + } + + return data; + } catch (error) { + console.error('Signup step two error:', error); + throw error; + } + }, + + forgotPassword: async (email) => { + try { + const response = await fetch(`${API_URL}${AUTH_ENDPOINTS.forgotPassword}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Password reset request failed'); + } + + return data; + } catch (error) { + console.error('Forgot password error:', error); + throw error; + } + }, + + resetPassword: async (uidb64, token, newPassword) => { + try { + const response = await fetch(`${API_URL}${AUTH_ENDPOINTS.resetPassword}/${uidb64}/${token}/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ new_password: newPassword }) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Password reset failed'); + } + return data; + + } catch (error) { + console.error('Reset password error:', error); + throw error; + } + } +}; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 91ee6be..b43de12 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@expo/metro-runtime": "~4.0.0", "@expo/vector-icons": "^14.0.4", + "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-community/hooks": "^3.0.0", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", @@ -3350,6 +3351,18 @@ "node": ">=14" } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz", + "integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native-community/hooks": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@react-native-community/hooks/-/hooks-3.0.0.tgz", @@ -7130,6 +7143,15 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -8032,6 +8054,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 437a173..d439714 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "dependencies": { "@expo/metro-runtime": "~4.0.0", "@expo/vector-icons": "^14.0.4", + "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-community/hooks": "^3.0.0", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 91c4cab..3ae481b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1645,6 +1645,13 @@ resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@react-native-async-storage/async-storage@^2.1.2": + version "2.1.2" + resolved "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz" + integrity sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow== + dependencies: + merge-options "^3.0.4" + "@react-native-community/hooks@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@react-native-community/hooks/-/hooks-3.0.0.tgz" @@ -4060,6 +4067,11 @@ is-path-inside@^3.0.2: resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" @@ -4347,10 +4359,10 @@ lighthouse-logger@^1.0.0: debug "^2.6.9" marky "^1.2.2" -lightningcss-win32-x64-msvc@1.27.0: +lightningcss-darwin-arm64@1.27.0: version "1.27.0" - resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz" - integrity sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw== + resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz" + integrity sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ== lightningcss@~1.27.0: version "1.27.0" @@ -4499,6 +4511,13 @@ memoize-one@^6.0.0: resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" @@ -5504,7 +5523,7 @@ react-native-web@~0.19.13: postcss-value-parser "^4.2.0" styleq "^0.1.3" -react-native@*, react-native@^0.76.5, react-native@>=0.42.0, react-native@>=0.56, react-native@>=0.61.0, react-native@>=0.65: +react-native@*, "react-native@^0.0.0-0 || >=0.65 <1.0", react-native@^0.76.5, react-native@>=0.42.0, react-native@>=0.56, react-native@>=0.61.0, react-native@>=0.65: version "0.76.5" resolved "https://registry.npmjs.org/react-native/-/react-native-0.76.5.tgz" integrity sha512-op2p2kB+lqMF1D7AdX4+wvaR0OPFbvWYs+VBE7bwsb99Cn9xISrLRLAgFflZedQsa5HvnOGrULhtnmItbIKVVw==