Files
ASP.NET/02_ProjectOrientedSessions/Session07/Session07 Frontend.md
T
2025-06-07 00:11:34 +03:30

802 lines
17 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Branching
- [ ] Create the feature/authentication branch based on develop
# Adding Models
Create three files inside this folder:
📂 Suggested Folder: shared/models/authentication
- [ ] `AuthResponseDto.ts`
```ts
export interface AuthResponseDto {
  token: string;
  phoneNumber: string;
  roles: string[];
}
```
- [ ] `LoginRequestDto.ts`
```ts
export interface LoginRequestDto {
  phoneNumber: string;
  password: string;
}
```
- [ ] `RegisterRequestDto.ts`
```ts
export interface RegisterRequestDto {
  phoneNumber: string;
  password: string;
  confirmPassword: string;
}
```
# Adding Authentication API Calls in `agent.ts`
- [ ] Add the `Auth` object to `agent.ts`:
```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:
```tsx
const agent = {
    TransportationSearch,
    Cities,
    Auth
}
```
# Creating `authStore.ts`
Suggested Folder
📂 Suggested Folder: shared/store/
- [ ] Create authStore.ts
```tsx
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`](https://github.com/pmndrs/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
```ts
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
```ts
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; otherwise `null`.
- `token`: JWT or access token from the server.
- `login()`: Accepts an `AuthResponseDto` and updates the state.
- `logout()`: Clears all authentication-related data.
- `setToken()`: Sets the token and toggles login status accordingly.
---
## 🔹 Zustand Store Definition
```ts
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
```ts
isLoggedIn: false,
user: null,
token: null,
```
These lines define the initial, default state for an unauthenticated user.
---
## 🔹 `login()` Method
```ts
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`, and `roles`, and sets them in state.
- Marks the user as `isLoggedIn: true`.
---
## 🔹 `logout()` Method
```ts
logout: () =>
set(() => ({
token: null,
user: null,
isLoggedIn: false,
})),
```
- Clears all auth-related data (token and user).
- Effectively logs the user out by setting `isLoggedIn` to `false`.
---
## 🔹 `setToken()` Method
```ts
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
```tsx
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
```tsx
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
```tsx
const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => { ... };
```
Defines a functional React component with the `onClose` prop destructured.
### 🔸 Accessing Auth Store
```tsx
const login = useAuthStore((state) => state.login);
```
Retrieves the `login` method from Zustands `authStore` so that the global auth state can be updated after successful login.
### 🔸 Local Form State
```tsx
const [form, setForm] = useState<LoginRequestDto>({
phoneNumber: "",
password: "",
});
```
Initializes `form` state with empty values for the phone number and password.
### 🔸 Error Handling State
```tsx
const [error, setError] = useState<string | null>(null);
```
Stores any error messages resulting from validation or login attempt.
---
## 🔹 Validation Logic
```tsx
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
```tsx
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
```tsx
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
```tsx
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
```tsx
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**
```tsx
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**
```tsx
const [form, setForm] = useState<RegisterRequestDto>({
  phoneNumber: "",
  password: "",
  confirmPassword: "",
});
```
- Initializes the form state for inputs, based on the `RegisterRequestDto` shape.
```tsx
const [error, setError] = useState<string | null>(null);
```
- Stores any validation or server error message to display in the UI.
```tsx
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**
```tsx
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**
```tsx
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 input `name`.
---
### 5. **Form Submission**
```tsx
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**
```tsx
<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`
---
# Merge
- [ ] Create a PR and merge the current branch with develop