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" }); } };