refactor: change folder structure
This commit is contained in:
@@ -0,0 +1,803 @@
|
||||
# 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 Zustand’s `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`
|
||||
|
||||
---
|
||||
|
||||
# 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
|
||||
Reference in New Issue
Block a user