831 lines
19 KiB
Markdown
831 lines
19 KiB
Markdown
# Users Module
|
|
|
|
## Overview
|
|
|
|
The Users module provides comprehensive user management with role-based access control, pagination, search, and filtering. It supports CRUD operations with granular permissions allowing admins to manage all users while regular users can only view/update their own profile.
|
|
|
|
**Key Features:**
|
|
|
|
- Full CRUD operations with role-based permissions
|
|
- Paginated list with search (email, name) and filters (role, status)
|
|
- Self-service profile updates for regular users
|
|
- Admin-only role and status changes
|
|
- Password hashing with bcrypt (12 salt rounds)
|
|
- Temporary user expiration handling
|
|
- Email uniqueness validation
|
|
- Cascading delete for related records
|
|
|
|
## File Paths
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `api/src/modules/users/users.routes.ts` | Express router with 5 CRUD endpoints |
|
|
| `api/src/modules/users/users.service.ts` | User management business logic |
|
|
| `api/src/modules/users/users.schemas.ts` | Zod validation schemas |
|
|
|
|
## Database Model
|
|
|
|
The Users module uses the `User` model from the Auth module:
|
|
|
|
```prisma
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
password String
|
|
name String?
|
|
phone String?
|
|
role UserRole @default(USER)
|
|
status UserStatus @default(ACTIVE)
|
|
permissions Json?
|
|
createdVia String? @default("web")
|
|
emailVerified Boolean @default(false)
|
|
expiresAt DateTime? // For TEMP users
|
|
expireDays Int? // Days until expiration
|
|
lastLoginAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
refreshTokens RefreshToken[]
|
|
createdCampaigns Campaign[] @relation("CreatedBy")
|
|
createdLocations Location[] @relation("CreatedBy")
|
|
// ... other relations
|
|
}
|
|
|
|
enum UserRole {
|
|
SUPER_ADMIN // Full system access
|
|
INFLUENCE_ADMIN // Campaign management
|
|
MAP_ADMIN // Location/canvass management
|
|
USER // Standard authenticated user
|
|
TEMP // Temporary user (e.g., shift signups)
|
|
}
|
|
|
|
enum UserStatus {
|
|
ACTIVE // Normal operation
|
|
SUSPENDED // Temporarily disabled
|
|
BANNED // Permanently disabled
|
|
}
|
|
```
|
|
|
|
## API Endpoints
|
|
|
|
| Method | Path | Auth | Permissions | Description |
|
|
|--------|------|------|-------------|-------------|
|
|
| GET | `/api/users` | Required | Admin roles | List users with pagination/filters |
|
|
| GET | `/api/users/:id` | Required | Admin or self | Get user by ID |
|
|
| POST | `/api/users` | Required | Admin roles | Create new user |
|
|
| PUT | `/api/users/:id` | Required | Admin or self | Update user |
|
|
| DELETE | `/api/users/:id` | Required | Admin roles | Delete user |
|
|
|
|
**Admin Roles:** `SUPER_ADMIN`, `INFLUENCE_ADMIN`, `MAP_ADMIN`
|
|
|
|
## Endpoint Details
|
|
|
|
### GET /api/users
|
|
|
|
List users with pagination, search, and filtering (admin only).
|
|
|
|
**Request Headers:**
|
|
|
|
```
|
|
Authorization: Bearer <access_token>
|
|
```
|
|
|
|
**Query Parameters:**
|
|
|
|
| Parameter | Type | Required | Default | Description |
|
|
|-----------|------|----------|---------|-------------|
|
|
| page | number | No | 1 | Page number (1-indexed) |
|
|
| limit | number | No | 20 | Results per page (max 100) |
|
|
| search | string | No | - | Search email or name (case-insensitive) |
|
|
| role | UserRole | No | - | Filter by role |
|
|
| status | UserStatus | No | - | Filter by status |
|
|
|
|
**Example Request:**
|
|
|
|
```bash
|
|
curl -H "Authorization: Bearer <token>" \
|
|
"http://api.cmlite.org/api/users?page=1&limit=20&search=john&role=USER&status=ACTIVE"
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
|
|
```json
|
|
{
|
|
"users": [
|
|
{
|
|
"id": "clx1234567890",
|
|
"email": "john.doe@example.com",
|
|
"name": "John Doe",
|
|
"phone": "+1234567890",
|
|
"role": "USER",
|
|
"status": "ACTIVE",
|
|
"permissions": null,
|
|
"createdVia": "web",
|
|
"expiresAt": null,
|
|
"expireDays": null,
|
|
"lastLoginAt": "2026-02-11T12:00:00.000Z",
|
|
"emailVerified": true,
|
|
"createdAt": "2026-02-01T12:00:00.000Z",
|
|
"updatedAt": "2026-02-11T12:00:00.000Z"
|
|
}
|
|
],
|
|
"pagination": {
|
|
"page": 1,
|
|
"limit": 20,
|
|
"total": 150,
|
|
"totalPages": 8
|
|
}
|
|
}
|
|
```
|
|
|
|
**Implementation:**
|
|
|
|
```typescript
|
|
router.get(
|
|
'/',
|
|
requireRole(...ADMIN_ROLES),
|
|
validate(listUsersSchema, 'query'),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
const result = await usersService.findAll(req.query as any);
|
|
res.json(result);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
}
|
|
);
|
|
```
|
|
|
|
**Search Logic:**
|
|
|
|
```typescript
|
|
if (search) {
|
|
where.OR = [
|
|
{ email: { contains: search, mode: 'insensitive' } },
|
|
{ name: { contains: search, mode: 'insensitive' } },
|
|
];
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### GET /api/users/:id
|
|
|
|
Get user by ID. Admins can view any user, regular users can only view themselves.
|
|
|
|
**Request Headers:**
|
|
|
|
```
|
|
Authorization: Bearer <access_token>
|
|
```
|
|
|
|
**Path Parameters:**
|
|
|
|
- `id` (string): User ID
|
|
|
|
**Example Request:**
|
|
|
|
```bash
|
|
curl -H "Authorization: Bearer <token>" \
|
|
"http://api.cmlite.org/api/users/clx1234567890"
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
|
|
```json
|
|
{
|
|
"id": "clx1234567890",
|
|
"email": "john.doe@example.com",
|
|
"name": "John Doe",
|
|
"phone": "+1234567890",
|
|
"role": "USER",
|
|
"status": "ACTIVE",
|
|
"permissions": null,
|
|
"createdVia": "web",
|
|
"expiresAt": null,
|
|
"expireDays": null,
|
|
"lastLoginAt": "2026-02-11T12:00:00.000Z",
|
|
"emailVerified": true,
|
|
"createdAt": "2026-02-01T12:00:00.000Z",
|
|
"updatedAt": "2026-02-11T12:00:00.000Z"
|
|
}
|
|
```
|
|
|
|
**Error Responses:**
|
|
|
|
- `403 Forbidden`: Non-admin trying to view another user
|
|
- `404 Not Found`: User ID does not exist
|
|
|
|
**Permission Logic:**
|
|
|
|
```typescript
|
|
const isAdmin = ADMIN_ROLES.includes(req.user!.role);
|
|
const isSelf = req.user!.id === id;
|
|
|
|
if (!isAdmin && !isSelf) {
|
|
res.status(403).json({ error: { message: 'Insufficient permissions', code: 'FORBIDDEN' } });
|
|
return;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### POST /api/users
|
|
|
|
Create new user account (admin only). Unlike public registration, admins can set any role.
|
|
|
|
**Request Headers:**
|
|
|
|
```
|
|
Authorization: Bearer <access_token>
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Request Body:**
|
|
|
|
```json
|
|
{
|
|
"email": "newuser@example.com",
|
|
"password": "TempPass123",
|
|
"name": "New User",
|
|
"phone": "+1234567890",
|
|
"role": "MAP_ADMIN",
|
|
"status": "ACTIVE",
|
|
"expiresAt": "2026-12-31T23:59:59Z",
|
|
"expireDays": 365
|
|
}
|
|
```
|
|
|
|
**Field Details:**
|
|
|
|
| Field | Type | Required | Description |
|
|
|-------|------|----------|-------------|
|
|
| email | string | Yes | Unique email address |
|
|
| password | string | Yes | Minimum 8 characters (admin creation has relaxed policy) |
|
|
| name | string | No | Full name |
|
|
| phone | string | No | Phone number |
|
|
| role | UserRole | No | Default: USER |
|
|
| status | UserStatus | No | Default: ACTIVE |
|
|
| expiresAt | ISO 8601 | No | Expiration timestamp (for TEMP users) |
|
|
| expireDays | number | No | Days until expiration |
|
|
|
|
**Response (201 Created):**
|
|
|
|
```json
|
|
{
|
|
"id": "clx0987654321",
|
|
"email": "newuser@example.com",
|
|
"name": "New User",
|
|
"phone": "+1234567890",
|
|
"role": "MAP_ADMIN",
|
|
"status": "ACTIVE",
|
|
"permissions": null,
|
|
"createdVia": "web",
|
|
"expiresAt": "2026-12-31T23:59:59.000Z",
|
|
"expireDays": 365,
|
|
"lastLoginAt": null,
|
|
"emailVerified": false,
|
|
"createdAt": "2026-02-11T12:00:00.000Z",
|
|
"updatedAt": "2026-02-11T12:00:00.000Z"
|
|
}
|
|
```
|
|
|
|
**Error Responses:**
|
|
|
|
- `409 Conflict`: Email already registered
|
|
- `403 Forbidden`: Non-admin trying to create user
|
|
|
|
---
|
|
|
|
### PUT /api/users/:id
|
|
|
|
Update user. Admins can update any user and change role/status. Regular users can update their own profile (except role/status).
|
|
|
|
**Request Headers:**
|
|
|
|
```
|
|
Authorization: Bearer <access_token>
|
|
Content-Type: application/json
|
|
```
|
|
|
|
**Path Parameters:**
|
|
|
|
- `id` (string): User ID to update
|
|
|
|
**Request Body (Partial Update):**
|
|
|
|
```json
|
|
{
|
|
"name": "Updated Name",
|
|
"phone": "+0987654321",
|
|
"email": "newemail@example.com",
|
|
"password": "NewPass123",
|
|
"role": "INFLUENCE_ADMIN",
|
|
"status": "SUSPENDED"
|
|
}
|
|
```
|
|
|
|
**All fields are optional (partial updates supported).**
|
|
|
|
**Response (200 OK):**
|
|
|
|
```json
|
|
{
|
|
"id": "clx1234567890",
|
|
"email": "newemail@example.com",
|
|
"name": "Updated Name",
|
|
"phone": "+0987654321",
|
|
"role": "INFLUENCE_ADMIN",
|
|
"status": "SUSPENDED",
|
|
...
|
|
}
|
|
```
|
|
|
|
**Error Responses:**
|
|
|
|
- `403 Forbidden`: Non-admin trying to update another user or change role/status
|
|
- `404 Not Found`: User ID does not exist
|
|
- `409 Conflict`: Email already in use by another user
|
|
|
|
**Permission Logic:**
|
|
|
|
```typescript
|
|
const isAdmin = ADMIN_ROLES.includes(req.user!.role);
|
|
const isSelf = req.user!.id === id;
|
|
|
|
if (!isAdmin && !isSelf) {
|
|
return res.status(403).json({ error: { message: 'Insufficient permissions', code: 'FORBIDDEN' } });
|
|
}
|
|
|
|
// Non-admins cannot change role or status
|
|
if (!isAdmin) {
|
|
delete req.body.role;
|
|
delete req.body.status;
|
|
}
|
|
```
|
|
|
|
**Password Handling:**
|
|
|
|
```typescript
|
|
if (data.password) {
|
|
updateData.password = await bcrypt.hash(data.password, 12);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### DELETE /api/users/:id
|
|
|
|
Delete user (admin only). Cascades to related records (refresh tokens, created campaigns, etc.).
|
|
|
|
**Request Headers:**
|
|
|
|
```
|
|
Authorization: Bearer <access_token>
|
|
```
|
|
|
|
**Path Parameters:**
|
|
|
|
- `id` (string): User ID to delete
|
|
|
|
**Example Request:**
|
|
|
|
```bash
|
|
curl -X DELETE \
|
|
-H "Authorization: Bearer <token>" \
|
|
"http://api.cmlite.org/api/users/clx1234567890"
|
|
```
|
|
|
|
**Response (204 No Content):**
|
|
|
|
No response body.
|
|
|
|
**Error Responses:**
|
|
|
|
- `403 Forbidden`: Non-admin trying to delete user
|
|
- `404 Not Found`: User ID does not exist
|
|
|
|
**Cascading Deletes:**
|
|
|
|
Deleting a user automatically deletes:
|
|
- Refresh tokens
|
|
- Created campaigns (if `createdByUserId` relation)
|
|
- Created locations (if `createdByUserId` relation)
|
|
- Campaign emails
|
|
- Responses
|
|
- Shift signups
|
|
|
|
## Service Functions
|
|
|
|
### usersService.findAll(filters)
|
|
|
|
**Purpose:** Paginated user listing with search and filters.
|
|
|
|
**Parameters:**
|
|
|
|
```typescript
|
|
interface ListUsersInput {
|
|
page: number; // Default: 1
|
|
limit: number; // Default: 20, max: 100
|
|
search?: string; // Search email or name
|
|
role?: UserRole; // Filter by role
|
|
status?: UserStatus; // Filter by status
|
|
}
|
|
```
|
|
|
|
**Returns:**
|
|
|
|
```typescript
|
|
{
|
|
users: User[];
|
|
pagination: {
|
|
page: number;
|
|
limit: number;
|
|
total: number;
|
|
totalPages: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
**Implementation:**
|
|
|
|
```typescript
|
|
const { page, limit, search, role, status } = filters;
|
|
const skip = (page - 1) * limit;
|
|
|
|
const where: Prisma.UserWhereInput = {};
|
|
|
|
if (search) {
|
|
where.OR = [
|
|
{ email: { contains: search, mode: 'insensitive' } },
|
|
{ name: { contains: search, mode: 'insensitive' } },
|
|
];
|
|
}
|
|
|
|
if (role) where.role = role;
|
|
if (status) where.status = status;
|
|
|
|
const [users, total] = await Promise.all([
|
|
prisma.user.findMany({
|
|
where,
|
|
select: userSelect,
|
|
skip,
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
}),
|
|
prisma.user.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
users,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit),
|
|
},
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### usersService.findById(id)
|
|
|
|
**Purpose:** Get single user by ID.
|
|
|
|
**Returns:** User object or throws `404` error.
|
|
|
|
**Security:** Password excluded via `select` (never returned in API responses).
|
|
|
|
---
|
|
|
|
### usersService.create(data)
|
|
|
|
**Purpose:** Create new user with hashed password.
|
|
|
|
**Flow:**
|
|
|
|
1. Check if email already exists (`409` if duplicate)
|
|
2. Hash password with bcrypt (12 salt rounds)
|
|
3. Create user in database
|
|
4. Return user (password excluded)
|
|
|
|
**Expiration Handling:**
|
|
|
|
```typescript
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
...data,
|
|
password: hashedPassword,
|
|
expiresAt: data.expiresAt ? new Date(data.expiresAt) : undefined,
|
|
},
|
|
select: userSelect,
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### usersService.update(id, data)
|
|
|
|
**Purpose:** Update existing user (partial updates supported).
|
|
|
|
**Validation:**
|
|
|
|
- Check user exists (`404` if not found)
|
|
- Check email uniqueness if changing email (`409` if taken)
|
|
- Hash password if provided
|
|
- Convert `expiresAt` string to Date
|
|
|
|
**Email Change:**
|
|
|
|
```typescript
|
|
if (data.email && data.email !== existing.email) {
|
|
const emailTaken = await prisma.user.findUnique({ where: { email: data.email } });
|
|
if (emailTaken) {
|
|
throw new AppError(409, 'Email already in use', 'EMAIL_EXISTS');
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### usersService.delete(id)
|
|
|
|
**Purpose:** Delete user and cascade to related records.
|
|
|
|
**Error Handling:**
|
|
|
|
```typescript
|
|
const existing = await prisma.user.findUnique({ where: { id } });
|
|
if (!existing) {
|
|
throw new AppError(404, 'User not found', 'USER_NOT_FOUND');
|
|
}
|
|
|
|
await prisma.user.delete({ where: { id } });
|
|
```
|
|
|
|
## Code Examples
|
|
|
|
### Admin: List Users with Filters
|
|
|
|
```typescript
|
|
import { api } from '@/lib/api';
|
|
|
|
const fetchUsers = async (page = 1, search = '', role = null, status = null) => {
|
|
const params = new URLSearchParams({
|
|
page: page.toString(),
|
|
limit: '20',
|
|
});
|
|
|
|
if (search) params.append('search', search);
|
|
if (role) params.append('role', role);
|
|
if (status) params.append('status', status);
|
|
|
|
const { data } = await api.get(`/api/users?${params}`);
|
|
return data;
|
|
};
|
|
|
|
// Usage
|
|
const result = await fetchUsers(1, 'john', 'USER', 'ACTIVE');
|
|
console.log(`Total users: ${result.pagination.total}`);
|
|
console.log(`Users on page 1:`, result.users);
|
|
```
|
|
|
|
### Admin: Create User
|
|
|
|
```typescript
|
|
import { api } from '@/lib/api';
|
|
|
|
const createUser = async (userData) => {
|
|
const { data } = await api.post('/api/users', {
|
|
email: userData.email,
|
|
password: userData.password,
|
|
name: userData.name,
|
|
phone: userData.phone,
|
|
role: userData.role || 'USER',
|
|
status: userData.status || 'ACTIVE',
|
|
});
|
|
|
|
return data;
|
|
};
|
|
|
|
// Usage
|
|
const newUser = await createUser({
|
|
email: 'volunteer@example.com',
|
|
password: 'TempPass123',
|
|
name: 'Jane Volunteer',
|
|
role: 'USER',
|
|
});
|
|
|
|
console.log(`Created user: ${newUser.id}`);
|
|
```
|
|
|
|
### Admin: Update User Role
|
|
|
|
```typescript
|
|
import { api } from '@/lib/api';
|
|
|
|
const promoteToAdmin = async (userId: string, adminRole: string) => {
|
|
const { data } = await api.put(`/api/users/${userId}`, {
|
|
role: adminRole,
|
|
});
|
|
|
|
return data;
|
|
};
|
|
|
|
// Usage
|
|
const updatedUser = await promoteToAdmin('clx1234567890', 'MAP_ADMIN');
|
|
console.log(`User promoted to ${updatedUser.role}`);
|
|
```
|
|
|
|
### User: Update Own Profile
|
|
|
|
```typescript
|
|
import { api } from '@/lib/api';
|
|
|
|
const updateProfile = async (name: string, phone: string) => {
|
|
const { data } = await api.put(`/api/users/${currentUser.id}`, {
|
|
name,
|
|
phone,
|
|
});
|
|
|
|
return data;
|
|
};
|
|
|
|
// Usage (non-admin user updating self)
|
|
const updated = await updateProfile('Updated Name', '+1234567890');
|
|
console.log('Profile updated:', updated);
|
|
```
|
|
|
|
### Admin: Suspend User
|
|
|
|
```typescript
|
|
import { api } from '@/lib/api';
|
|
|
|
const suspendUser = async (userId: string) => {
|
|
const { data } = await api.put(`/api/users/${userId}`, {
|
|
status: 'SUSPENDED',
|
|
});
|
|
|
|
return data;
|
|
};
|
|
|
|
// Usage
|
|
const suspended = await suspendUser('clx1234567890');
|
|
console.log(`User ${suspended.email} suspended`);
|
|
```
|
|
|
|
### Admin: Delete User
|
|
|
|
```typescript
|
|
import { api } from '@/lib/api';
|
|
|
|
const deleteUser = async (userId: string) => {
|
|
await api.delete(`/api/users/${userId}`);
|
|
console.log(`User ${userId} deleted`);
|
|
};
|
|
|
|
// Usage
|
|
await deleteUser('clx1234567890');
|
|
```
|
|
|
|
## Frontend Integration
|
|
|
|
The UsersPage component (`admin/src/pages/UsersPage.tsx`) provides a comprehensive UI for user management:
|
|
|
|
**Features:**
|
|
|
|
- Paginated table with role/status badges
|
|
- Search by email or name (300ms debounce)
|
|
- Filter dropdowns (role, status)
|
|
- Create user modal with form validation
|
|
- Edit user modal (pre-populated form)
|
|
- Delete confirmation modal
|
|
- Responsive design (mobile-friendly)
|
|
|
|
**State Management:**
|
|
|
|
```typescript
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [pagination, setPagination] = useState({ page: 1, limit: 20, total: 0, totalPages: 0 });
|
|
const [filters, setFilters] = useState({ search: '', role: null, status: null });
|
|
```
|
|
|
|
**API Integration:**
|
|
|
|
```typescript
|
|
const fetchUsers = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const params = new URLSearchParams({
|
|
page: pagination.page.toString(),
|
|
limit: pagination.limit.toString(),
|
|
});
|
|
|
|
if (filters.search) params.append('search', filters.search);
|
|
if (filters.role) params.append('role', filters.role);
|
|
if (filters.status) params.append('status', filters.status);
|
|
|
|
const { data } = await api.get(`/api/users?${params}`);
|
|
setUsers(data.users);
|
|
setPagination(data.pagination);
|
|
} catch (error) {
|
|
message.error('Failed to fetch users');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
```
|
|
|
|
## Validation Schemas
|
|
|
|
### Create User Schema
|
|
|
|
```typescript
|
|
export const createUserSchema = z.object({
|
|
email: z.string().email(),
|
|
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
name: z.string().optional(),
|
|
phone: z.string().optional(),
|
|
role: z.nativeEnum(UserRole).optional(),
|
|
status: z.nativeEnum(UserStatus).optional(),
|
|
expiresAt: z.string().datetime().optional(),
|
|
expireDays: z.number().int().positive().optional(),
|
|
});
|
|
```
|
|
|
|
**Note:** Admin user creation has relaxed password requirements (8 chars vs. 12 for public registration).
|
|
|
|
### Update User Schema
|
|
|
|
```typescript
|
|
export const updateUserSchema = z.object({
|
|
email: z.string().email().optional(),
|
|
password: z.string().min(8).optional(),
|
|
name: z.string().optional(),
|
|
phone: z.string().optional(),
|
|
role: z.nativeEnum(UserRole).optional(),
|
|
status: z.nativeEnum(UserStatus).optional(),
|
|
expiresAt: z.string().datetime().nullable().optional(),
|
|
expireDays: z.number().int().positive().nullable().optional(),
|
|
});
|
|
```
|
|
|
|
### List Users Schema
|
|
|
|
```typescript
|
|
export const listUsersSchema = z.object({
|
|
page: z.coerce.number().int().positive().default(1),
|
|
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
search: z.string().optional(),
|
|
role: z.nativeEnum(UserRole).optional(),
|
|
status: z.nativeEnum(UserStatus).optional(),
|
|
});
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
### Password Security
|
|
|
|
- **Hashing**: bcrypt with 12 salt rounds (admin creation) or enforced by registration schema
|
|
- **Never Returned**: Password excluded from all API responses via `select` clause
|
|
- **Updates**: Re-hashed when changed
|
|
|
|
### Permission Model
|
|
|
|
| Action | SUPER_ADMIN | INFLUENCE_ADMIN | MAP_ADMIN | USER |
|
|
|--------|-------------|-----------------|-----------|------|
|
|
| List users | ✅ | ✅ | ✅ | ❌ |
|
|
| View any user | ✅ | ✅ | ✅ | Own profile only |
|
|
| Create user | ✅ | ✅ | ✅ | ❌ |
|
|
| Update any user | ✅ | ✅ | ✅ | Own profile only |
|
|
| Change role/status | ✅ | ✅ | ✅ | ❌ |
|
|
| Delete user | ✅ | ✅ | ✅ | ❌ |
|
|
|
|
### Email Uniqueness
|
|
|
|
- Enforced at database level (`@unique` constraint)
|
|
- Checked before creation (`409 Conflict`)
|
|
- Checked before email change (`409 Conflict`)
|
|
|
|
### Cascading Deletes
|
|
|
|
Deleting a user automatically deletes related records via Prisma `onDelete: Cascade`:
|
|
|
|
- RefreshTokens
|
|
- Created campaigns
|
|
- Created locations
|
|
- Campaign emails
|
|
- Responses
|
|
- Shift signups
|
|
|
|
## Related Documentation
|
|
|
|
- [Auth Module](/v2/backend/modules/auth.md) - Authentication and JWT tokens
|
|
- [Middleware: RBAC](/v2/backend/middleware/rbac.md) - Role-based access control
|
|
- [Frontend: UsersPage](/v2/frontend/pages/admin/users-page.md) - User management UI
|
|
- [Database: User Model](/v2/database/models/auth.md) - User schema documentation
|
|
- [API Reference: Users](/v2/api-reference/users.md) - Complete endpoint reference
|
|
- [User Guide: Admin](/v2/user-guides/admin-guide.md) - Managing users guide
|