From 08139ca68247228f5e934394484ffbc32ada6701 Mon Sep 17 00:00:00 2001 From: Arka Jyoti Adhikary <66660068+arkajyotiadhikary@users.noreply.github.com> Date: Tue, 2 Apr 2024 01:07:35 +0530 Subject: [PATCH] Add private routes for React Native client Implemented private routes to protect sensitive screens and functionalities in the React Native client. Used authentication context to conditionally render components based on user authentication status. --- client/package-lock.json | 8 ++-- client/package.json | 2 +- client/src/App.tsx | 31 +----------- client/src/contexts/mAuth.context.tsx | 22 +++++++++ client/src/features/user/userSlice.ts | 22 +++++++++ client/src/routes/Router.tsx | 15 ++++++ client/src/screens/Auth.screen.tsx | 57 ++++++++++++++++------- client/src/services/authService.ts | 50 ++++++++++++++------ client/src/services/songService.ts | 11 ++++- client/src/stacks/mAuth.stack.tsx | 30 ++++++++++++ client/src/store.ts | 3 ++ server/src/controllers/auth.controller.ts | 13 ++++-- 12 files changed, 193 insertions(+), 71 deletions(-) create mode 100644 client/src/contexts/mAuth.context.tsx create mode 100644 client/src/features/user/userSlice.ts create mode 100644 client/src/routes/Router.tsx create mode 100644 client/src/stacks/mAuth.stack.tsx diff --git a/client/package-lock.json b/client/package-lock.json index e3e92af..d15f2b3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@expo/metro-runtime": "~3.1.3", "@expo/vector-icons": "^14.0.0", - "@react-native-async-storage/async-storage": "^1.23.0", + "@react-native-async-storage/async-storage": "^1.23.1", "@react-native-community/slider": "^4.5.0", "@react-navigation/native": "^6.1.14", "@react-navigation/stack": "^6.3.25", @@ -3153,9 +3153,9 @@ } }, "node_modules/@react-native-async-storage/async-storage": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.23.0.tgz", - "integrity": "sha512-Gmor1uChB46ATuRy8HDkRVbOEq0KtqM7+vR5/m76Izj+jSbpPGtNFbi1Cm6HpKJ0yAR2XdbAjYtUoQXmsf8Lxg==", + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz", + "integrity": "sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==", "dependencies": { "merge-options": "^3.0.4" }, diff --git a/client/package.json b/client/package.json index 9ccab70..3dade7d 100644 --- a/client/package.json +++ b/client/package.json @@ -11,7 +11,7 @@ "dependencies": { "@expo/metro-runtime": "~3.1.3", "@expo/vector-icons": "^14.0.0", - "@react-native-async-storage/async-storage": "^1.23.0", + "@react-native-async-storage/async-storage": "^1.23.1", "@react-native-community/slider": "^4.5.0", "@react-navigation/native": "^6.1.14", "@react-navigation/stack": "^6.3.25", diff --git a/client/src/App.tsx b/client/src/App.tsx index 9af495e..003fb52 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,40 +1,13 @@ import React from "react"; - -// navigaton import -import { NavigationContainer } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; -const stack = createStackNavigator(); - -// state store import { Provider } from "react-redux"; import { store } from "./store"; - -// screens -import Home from "./screens/Home.screen"; -import Auth from "./screens/Auth.screen"; -import PlayerScreen from "./screens/Player.screen"; -import Search from "./screens/Search.screen"; -import { RootStackNavigationProp } from "../types"; +import Router from "./routes/router"; export default function App() { return ( - - - - - - - - + ); } diff --git a/client/src/contexts/mAuth.context.tsx b/client/src/contexts/mAuth.context.tsx new file mode 100644 index 0000000..ae76681 --- /dev/null +++ b/client/src/contexts/mAuth.context.tsx @@ -0,0 +1,22 @@ +import React, { createContext, useContext, useState } from "react"; + +interface AuthContextData { + isAuthenticated: boolean; + setIsAuthenticated: React.Dispatch>; +} + +export const AuthContext = createContext(null); + +interface AuthProviderProps { + children: React.ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const value: AuthContextData = { isAuthenticated, setIsAuthenticated }; + + return ( + {children} + ); +}; diff --git a/client/src/features/user/userSlice.ts b/client/src/features/user/userSlice.ts new file mode 100644 index 0000000..d876306 --- /dev/null +++ b/client/src/features/user/userSlice.ts @@ -0,0 +1,22 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; + +interface User { + isAuthenticated: boolean; +} + +const initUserState: User = { + isAuthenticated: false, +}; + +export const userSlice = createSlice({ + name: "user", + initialState: initUserState, + reducers: { + setCurrentUser(state, action: PayloadAction) { + state.isAuthenticated = action.payload.isAuthenticated; + }, + }, +}); + +export const { setCurrentUser } = userSlice.actions; +export const userReducer = userSlice.reducer; diff --git a/client/src/routes/Router.tsx b/client/src/routes/Router.tsx new file mode 100644 index 0000000..5e34d50 --- /dev/null +++ b/client/src/routes/Router.tsx @@ -0,0 +1,15 @@ +import { NavigationContainer } from "@react-navigation/native"; +import { AppStack, AuthStack } from "../stacks/mAuth.stack"; +import { useSelector } from "react-redux"; +import { RootState } from "../store"; + +const Router = () => { + const userState = useSelector((state: RootState) => state.userReducer); + return ( + + {userState.isAuthenticated ? : } + + ); +}; + +export default Router; diff --git a/client/src/screens/Auth.screen.tsx b/client/src/screens/Auth.screen.tsx index 45278ef..0377bc6 100644 --- a/client/src/screens/Auth.screen.tsx +++ b/client/src/screens/Auth.screen.tsx @@ -1,14 +1,22 @@ import React, { FC, useState, useEffect } from "react"; import { View, Text, TextInput, TouchableOpacity, Image } from "react-native"; +import { useNavigation } from "@react-navigation/native"; import styles from "../styles/Auth.style"; import logo from "../assests/logo/wepik-export-20240324130518XYcX.png"; -import { type User } from "../../types"; +import { RootStackNavigationProp, type User } from "../../types"; import { signUp, signIn, validateUserInput } from "../services/authService"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { useDispatch } from "react-redux"; +import { setCurrentUser } from "../features/user/userSlice"; + const Auth: FC = () => { + const navigation = useNavigation(); + const dispatch = useDispatch(); + // a variable to check is it sign in or sign up form const [isSignIn, setIsSignIn] = useState(false); // a object to store user data from the input field @@ -40,22 +48,36 @@ const Auth: FC = () => { // method to handle submit . Check wheather you are submittin sign in form of sign up form const handleSubmit = async (): Promise => { + console.log("form data:", formData); const validation = validateUserInput(formData, isSignIn); - if (validation.isValid) { - if (isSignIn) signIn(formData.email, formData.password); - else { - const response = await signUp( + if (!validation.isValid) { + setErrorState(validation.message); + return; + } + + try { + let response; + if (isSignIn) { + response = await signIn(formData.email, formData.password); + if (!response?.hasError && "data" in response!) { + await AsyncStorage.setItem("token", response?.data?.token!); + dispatch(setCurrentUser({ isAuthenticated: true })); + } + } else { + response = await signUp( formData.username, formData.email, formData.password ); - if ("message" in response) { - console.log(response); - setErrorState(response?.message); - } } - } else { - setErrorState(validation.message); + + if ("message" in response!) { + setErrorState(response.message); + } + console.log("Response:", response); + } catch (error) { + setErrorState("An error occurred. Please try again."); // Handle generic error message + console.error("Error:", error); // Log the error for debugging } }; @@ -87,17 +109,18 @@ const Auth: FC = () => { handleChange("username", e)} + placeholder="Email" + value={formData.email} + onChangeText={(e) => handleChange("email", e)} /> + {!isSignIn && ( handleChange("email", e)} + placeholder="Username" + value={formData.username} + onChangeText={(e) => handleChange("username", e)} /> )} diff --git a/client/src/services/authService.ts b/client/src/services/authService.ts index 476335f..3573384 100644 --- a/client/src/services/authService.ts +++ b/client/src/services/authService.ts @@ -17,20 +17,21 @@ export const signUp = async ( ): Promise< | { hasError: boolean; User: User } | { hasError: boolean; message: string } - | unknown + | undefined > => { try { const response: AxiosResponse< | { hasError: boolean; User: User } | { hasError: boolean; message: string } + | undefined > = await axios.post(`${BASE_URL}/api/register`, { username, email, password, }); - return response.data; + return response.data || undefined; } catch (error) { - return handleAxiosError(error); + return handleAxiosError(error) as undefined; } }; @@ -39,18 +40,28 @@ export const signIn = async ( email: string | undefined, password: string | undefined ): Promise< - | { hasError: boolean; User: User } + | { + hasError: boolean; + data: { + role: string; + userVerified: boolean; + token: string; + }; + } | { hasError: boolean; message: string } | undefined > => { try { const response: AxiosResponse< - | { hasError: boolean; User: User } + | { + hasError: boolean; + data: { role: string; userVerified: boolean; token: string }; + } | { hasError: boolean; message: string } > = await axios.post(`${BASE_URL}/api/login`, { email, password }); - return response.data; + return response.data || undefined; } catch (error) { - handleAxiosError(error); + return handleAxiosError(error) as undefined; } }; @@ -61,8 +72,8 @@ export const validateUserInput = ( ): ValidateUserInput => { // Validate if the username, email, and password are not empty if ( - !formData.username || - (!formData.email && !isLogIn) || + (!formData.username && !isLogIn) || + !formData.email || !formData.password ) { return { @@ -72,11 +83,7 @@ export const validateUserInput = ( } // Validate the email if it's provided and it's not a login operation - if ( - !isLogIn && - formData.email && - !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) - ) { + if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { return { isValid: false, message: "Invalid email address.", @@ -96,3 +103,18 @@ export const validateUserInput = ( message: "", }; }; + +export const validateToken = async (token: string): Promise => { + try { + const response = await axios.post(`${BASE_URL}/api/validate`, { + token: token, + }); + if (!response.data.hasError) { + return true; + } + return false; + } catch (error) { + console.log(error); + return false; + } +}; diff --git a/client/src/services/songService.ts b/client/src/services/songService.ts index 3584c6c..97ed49a 100644 --- a/client/src/services/songService.ts +++ b/client/src/services/songService.ts @@ -2,6 +2,7 @@ import axios, { AxiosResponse } from "axios"; import { Song } from "../../types"; import { handleAxiosError } from "./axiosErrorHandler"; import { type AddTrack } from "react-native-track-player"; +import AsyncStorage from "@react-native-async-storage/async-storage"; const BASE_URL = "http://10.0.2.2:2526"; @@ -17,6 +18,7 @@ const calculateDurationInSeconds = (durationString: string | undefined) => { * Formate songs as AddTrack . * We can only add tracks of type AddTrack in TrackPlayer */ + const formateSong = (data: Song[]): AddTrack[] => { return data.map((song, index) => ({ url: song.AudioFilePath, @@ -31,9 +33,16 @@ const formateSong = (data: Song[]): AddTrack[] => { // Get all tracks export const getAllSong = async (): Promise => { + const token = await AsyncStorage.getItem("token"); + console.log("token", token); try { const response: AxiosResponse = await axios.get( - `${BASE_URL}/api/songs` + `${BASE_URL}/api/songs`, + { + headers: { + Authorization: `${token}`, + }, + } ); const formatedSong = formateSong(response.data); return formatedSong; diff --git a/client/src/stacks/mAuth.stack.tsx b/client/src/stacks/mAuth.stack.tsx new file mode 100644 index 0000000..24f3fc1 --- /dev/null +++ b/client/src/stacks/mAuth.stack.tsx @@ -0,0 +1,30 @@ +import { createStackNavigator } from "@react-navigation/stack"; +import { RootStackParamList } from "../../types"; +import Home from "../screens/Home.screen"; +import Auth from "../screens/Auth.screen"; + +const Stack = createStackNavigator(); + +export const AppStack = () => { + return ( + + + + ); +}; + +export const AuthStack = () => { + return ( + + + + ); +}; diff --git a/client/src/store.ts b/client/src/store.ts index 4101b79..271e948 100644 --- a/client/src/store.ts +++ b/client/src/store.ts @@ -7,11 +7,14 @@ import { songQueueReducer, } from "./features/song/songSlice"; +import { userReducer } from "./features/user/userSlice"; + export const store = configureStore({ reducer: { currentPlayingReducer, songControlsReducer, songQueueReducer, + userReducer, }, }); diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 84a81db..8047f53 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -102,15 +102,18 @@ export const loginUser = async (req: Request, res: Response) => { console.error(error); } res.status(200).json({ - role: user.role, - userVerified: true, - token: accessToken, + hasError: false, + data: { + role: user.role, + userVerified: true, + token: accessToken, + }, }); } else { - res.status(401).json({ message: "Invalid credentials" }); + res.status(401).json({ hasError: true, message: "Invalid credentials" }); } } catch (error) { console.error(error); - res.status(500).json({ message: "Internal server error" }); + res.status(500).json({ hasError: true, message: "User already exists" }); } };