Branching
- Create the feature/authentication branch based on develop
Adding Models
Create three files inside this folder: 📂 Suggested Folder: shared/models/authentication
-
AuthResponseDto.ts
export interface AuthResponseDto {
token: string;
phoneNumber: string;
roles: string[];
}
-
LoginRequestDto.ts
export interface LoginRequestDto {
phoneNumber: string;
password: string;
}
-
RegisterRequestDto.ts
export interface RegisterRequestDto {
phoneNumber: string;
password: string;
confirmPassword: string;
}
Adding Authentication API Calls in agent.ts
- Add the
Auth
object toagent.ts
:
const Auth = {
register: (data: RegisterRequestDto) =>
request.post<{ token: string }>('/auth/register', data),
login: (data: LoginRequestDto) =>
request.post<{ token: string }>('/auth/login', data),
};
- Ensure
agent.ts
ends like this:
const agent = {
TransportationSearch,
Cities,
Auth
}
Creating authStore.ts
Suggested Folder 📂 Suggested Folder: shared/store/
- Create authStore.ts
import { AuthResponseDto } from '@/shared/models/authentication/AuthResponseDto';
import {create} from 'zustand';
interface User {
phoneNumber: string;
roles: string[];
}
interface AuthState {
isLoggedIn: boolean;
user: User | null;
token: string | null;
login: (response: AuthResponseDto) => void;
logout: () => void;
setToken: (token: string) => void;
}
export const useAuthStore = create<AuthState>((set) => ({
isLoggedIn: false,
user: null,
token: null,
login: (response) =>
set(() => ({
token: response.token,
user: {
phoneNumber: response.phoneNumber,
roles: response.roles
},
isLoggedIn: true,
})),
logout: () =>
set(() => ({
token: null,
user: null,
isLoggedIn: false,
})),
setToken: (token) =>
set((state) => ({
token,
isLoggedIn: !!token,
user: state.user,
})),
}));
About authStore.ts
This file defines a centralized authentication state store using Zustand
, a minimal and scalable state management library for React. It helps manage login state, user data, and authentication token across your application.
🔹 User
Interface
interface User {
phoneNumber: string;
roles: string[];
}
This interface defines the shape of the user
object stored in the auth state. It currently includes:
phoneNumber
: A string representing the user’s phone number.roles
: An array representing user roles
🔹 AuthState
Interface
interface AuthState {
isLoggedIn: boolean;
user: User | null;
token: string | null;
login: (response: AuthResponseDto) => void;
logout: () => void;
setToken: (token: string) => void;
}
This defines the overall structure of the authentication store:
isLoggedIn
: Indicates whether a user is logged in.user
: Stores user-specific data if authenticated; otherwisenull
.token
: JWT or access token from the server.login()
: Accepts anAuthResponseDto
and updates the state.logout()
: Clears all authentication-related data.setToken()
: Sets the token and toggles login status accordingly.
🔹 Zustand Store Definition
export const useAuthStore = create<AuthState>((set) => ({ ... });
Creates a global auth store using Zustand. create()
accepts a function that receives set
(used to update state) and returns the initial store state and methods.
🔹 Initial State
isLoggedIn: false,
user: null,
token: null,
These lines define the initial, default state for an unauthenticated user.
🔹 login()
Method
login: (response) =>
set(() => ({
token: response.token,
user: {
phoneNumber: response.phoneNumber,
roles: response.roles
},
isLoggedIn: true,
})),
-
Accepts an
AuthResponseDto
object after a successful login. -
Extracts the
token
,phoneNumber
, androles
, and sets them in state. -
Marks the user as
isLoggedIn: true
.
🔹 logout()
Method
logout: () =>
set(() => ({
token: null,
user: null,
isLoggedIn: false,
})),
-
Clears all auth-related data (token and user).
-
Effectively logs the user out by setting
isLoggedIn
tofalse
.
🔹 setToken()
Method
setToken: (token) =>
set((state) => ({
token,
isLoggedIn: !!token,
user: state.user,
})),
-
Updates the token in the store.
-
Sets
isLoggedIn
based on whether a non-empty token exists. -
Retains the current
user
object.
Add LoginModal
📂 Suggested Folder: shared/features/authentication/modals
Example
import React, { useState } from "react";
import { useAuthStore } from "@/store/authStore";
import agent from "@/shared/api/agent";
import { LoginRequestDto } from "@/shared/models/authentication/LoginRequestDto";
interface LoginModalProps {
onClose: () => void;
}
const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => {
const login = useAuthStore((state) => state.login);
const [form, setForm] = useState<LoginRequestDto>({
phoneNumber: "",
password: "",
});
const [error, setError] = useState<string | null>(null);
const validate = () => {
const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
if (!phoneRegex.test(form.phoneNumber)) {
return "Invalid phone number format";
}
if (!form.password || form.password.length < 8) {
return "Password must be at least 8 characters";
}
return null;
};
const handleSubmit = async () => {
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
try {
const response = await agent.Auth.login(form);
login(response);
setError(null);
onClose();
} catch (err: any) {
setError(err.response?.data?.message || "Login failed");
}
};
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2 style={{ marginBottom: "1rem" }}>Login</h2>
<input
type="text"
placeholder="Phone Number"
value={form.phoneNumber}
onChange={(e) => setForm({ ...form, phoneNumber: e.target.value })}
style={styles.input}
/>
<input
type="password"
placeholder="Password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
style={styles.input}
/>
<button onClick={handleSubmit} style={styles.button}>
Login
</button>
{error && (
<p style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}>
{error}
</p>
)}
<button
onClick={onClose}
style={{
...styles.button,
marginTop: "0.5rem",
backgroundColor: "#ccc",
color: "#333",
}}
>
Cancel
</button>
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
//ADD STYLES
};
export default LoginModal;
About LoginModal
This component provides a modal UI that allows users to log in using their phone number and password. It integrates with the authentication store and API to perform login logic and handle errors.
🔹 Props Interface
interface LoginModalProps {
onClose: () => void;
}
onClose
: A callback to be called when the modal should be closed (e.g., user clicks “Cancel” or logs in successfully).
🔹 Component Setup
const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => { ... };
Defines a functional React component with the onClose
prop destructured.
🔸 Accessing Auth Store
const login = useAuthStore((state) => state.login);
Retrieves the login
method from Zustand’s authStore
so that the global auth state can be updated after successful login.
🔸 Local Form State
const [form, setForm] = useState<LoginRequestDto>({
phoneNumber: "",
password: "",
});
Initializes form
state with empty values for the phone number and password.
🔸 Error Handling State
const [error, setError] = useState<string | null>(null);
Stores any error messages resulting from validation or login attempt.
🔹 Validation Logic
const validate = () => {
const phoneRegex = /^(?:\+98|0)?9\d{9}$/;
if (!phoneRegex.test(form.phoneNumber)) {
return "Invalid phone number format";
}
if (!form.password || form.password.length < 8) {
return "Password must be at least 8 characters";
}
return null;
};
- Validates the phone number format (Iranian phone format in this case).
- Ensures password is at least 8 characters long.
- Returns a string error message or
null
if validation passes.
🔹 Submit Handler
const handleSubmit = async () => {
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
try {
const response = await agent.Auth.login(form);
login(response);
setError(null);
onClose();
} catch (err: any) {
setError(err.response?.data?.message || "Login failed");
}
};
- Calls
validate()
and prevents submission if there’s an error. - Calls the backend API using
agent.Auth.login()
. - On success: updates auth state, clears error, closes modal.
- On failure: shows error message.
🔹 UI Layout
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2>Login</h2>
<input ... />
<input ... />
<button onClick={handleSubmit}>Login</button>
{error && <p>{error}</p>}
<button onClick={onClose}>Cancel</button>
</div>
</div>
);
Elements:
-
Phone Number Input
-
Password Input
-
Login Button: Triggers
handleSubmit
. -
Error Message: Shown only if there’s an error.
-
Cancel Button: Triggers
onClose
callback.
🔹 Styles Placeholder
const styles: { [key: string]: React.CSSProperties } = {
// Add modal styles here
};
This placeholder defines inline CSS styles for the modal. Each style (e.g., overlay
, modal
, input
, button
) should be defined here.
✅ Summary
This modal:
-
Provides a simple, reusable login form.
-
Validates input before calling the API.
-
Updates global auth state via Zustand.
-
Handles success/failure states.
-
Uses modal-friendly inline styles (with room for improvement).
Add RegisterModal
- Create RegisterModal 📂 Suggested Folder: shared/features/authentication/modals
import agent from "@/shared/api/agent";
import { RegisterRequestDto } from "@/shared/models/authentication/RegisterRequestDto";
import { useAuthStore } from "@/store/authStore";
import React, { useState } from "react";
interface Props {
onClose: () => void;
}
const RegisterModal: React.FC<Props> = ({ onClose }) => {
const [form, setForm] = useState<RegisterRequestDto>({
phoneNumber: "",
password: "",
confirmPassword: "",
});
const [error, setError] = useState<string | null>(null);
const login = useAuthStore((state) => state.login);
const validate = () => {
const { phoneNumber, password, confirmPassword } = form;
if (!phoneNumber || !password || !confirmPassword) {
return "All fields are required.";
}
if (!/^\d{11}$/.test(phoneNumber)) {
return "Phone number must be 11 digits.";
}
if (password.length < 6) {
return "Password must be at least 6 characters.";
}
if (password !== confirmPassword) {
return "Passwords do not match.";
}
return null;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const validationError = validate();
if (validationError) {
setError(validationError);
return;
}
try {
// Use the form as RegisterRequestDto explicitly
const requestData: RegisterRequestDto = {
phoneNumber: form.phoneNumber,
password: form.password,
confirmPassword: form.confirmPassword,
};
const response = await agent.Auth.register(requestData);
login(response);
setForm({ phoneNumber: "", password: "", confirmPassword: "" });
onClose();
} catch (err: any) {
setError(err.response?.data?.message || "Registration failed.");
}
};
return (
<div style={styles.overlay}>
<div style={styles.modal}>
<h2 style={{ marginBottom: "1rem" }}>Register</h2>
<form
onSubmit={handleSubmit}
style={{ display: "flex", flexDirection: "column" }}
>
<input
type="text"
name="phoneNumber"
value={form.phoneNumber}
onChange={handleChange}
placeholder="Phone Number"
style={styles.input}
/>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
placeholder="Password"
style={styles.input}
/>
<input
type="password"
name="confirmPassword"
value={form.confirmPassword}
onChange={handleChange}
placeholder="Confirm Password"
style={styles.input}
/>
{error && (
<p
style={{ color: "red", marginTop: "0.5rem", fontWeight: "bold" }}
>
{error}
</p>
)}
<button type="submit" style={styles.button}>
Register
</button>
</form>
<button
onClick={onClose}
style={{
...styles.button,
marginTop: "0.5rem",
backgroundColor: "#ccc",
color: "#333",
}}
>
Cancel
</button>
</div>
</div>
);
};
const styles: { [key: string]: React.CSSProperties } = {
//ADD STYLES
};
export default RegisterModal;
About RegisterModal
Component Structure
1. Props
interface Props {
onClose: () => void;
}
- The modal only expects one prop:
onClose
, a function to close the modal (e.g., hide it from the screen).
2. State Management
const [form, setForm] = useState<RegisterRequestDto>({
phoneNumber: "",
password: "",
confirmPassword: "",
});
- Initializes the form state for inputs, based on the
RegisterRequestDto
shape.
const [error, setError] = useState<string | null>(null);
- Stores any validation or server error message to display in the UI.
const login = useAuthStore((state) => state.login);
- Accesses the
login
method from your global auth store, to automatically log in the user after successful registration.
3. Validation Logic
const validate = () => {
// Checks for empty fields
// Validates phone number format (must be 11 digits)
// Ensures password length is sufficient
// Confirms password and confirmation match
};
- Ensures client-side validation before making a request to the server.
4. Input Handling
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
- Updates the correct field in the
form
object dynamically based on the inputname
.
5. Form Submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
const validationError = validate();
// If validation passes, submit the data to the backend
// If backend response is successful, log in and close modal
// If it fails, show error message
};
- Prevents default form submission
- Validates inputs
- Sends the data to
agent.Auth.register
- On success: logs in user and clears form
- On failure: shows error from server
6. JSX Render
<div style={styles.overlay}>...</div>
- Modal Overlay: darkened background behind the modal
- Modal Box: contains title, form, and buttons
Inside <form>
:
-
Inputs for:
-phoneNumber
-password
-confirmPassword
-
Submit button for Register
-
Error message display (if any)
-
Cancel button that calls
onClose
Handle login/logout and register buttons in navbar
- implement a way for showing login and register buttons in navbar when user is not signed in
- when clicked, the button should show the related modal, for the user to sign in or register
Merge
- Create a PR and merge the current branch with develop