305 lines
9.0 KiB
Markdown
305 lines
9.0 KiB
Markdown
# Courrier User Management with Prisma
|
|
|
|
## Overview
|
|
|
|
**Important**: Courrier (the email system) does **NOT** create User records in Prisma. It only manages email account credentials (`MailCredentials`) for users that already exist in the database.
|
|
|
|
## User Creation Flow
|
|
|
|
### 1. User Creation in Keycloak (Primary Source)
|
|
|
|
Users are created in **Keycloak** first, which is the primary authentication system:
|
|
|
|
**Location**: `app/api/users/route.ts` (POST method)
|
|
|
|
**Process**:
|
|
1. User is created in Keycloak via Admin API
|
|
2. Roles are assigned to the user
|
|
3. User may be created in external systems:
|
|
- **Leantime** (project management tool)
|
|
- **Dolibarr** (if user has "Mediation" or "Expression" roles)
|
|
|
|
**Key Code**:
|
|
```typescript
|
|
// Create user in Keycloak
|
|
const createResponse = await fetch(
|
|
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
username: data.username,
|
|
enabled: true,
|
|
emailVerified: true,
|
|
firstName: data.firstName,
|
|
lastName: data.lastName,
|
|
email: data.email,
|
|
credentials: [{ type: "password", value: data.password, temporary: false }],
|
|
}),
|
|
}
|
|
);
|
|
```
|
|
|
|
### 2. User Sync to Prisma Database
|
|
|
|
After creation in Keycloak, users need to be synced to the Prisma database. This happens via:
|
|
|
|
**Option A: Manual Sync Script**
|
|
- `scripts/sync-users.ts` or `scripts/sync-users.js`
|
|
- Fetches users from Keycloak API
|
|
- Creates/updates User records in Prisma
|
|
|
|
**Option B: API Endpoint**
|
|
- `app/api/sync-users/route.ts` (GET method)
|
|
- Can be called to sync users programmatically
|
|
|
|
**Prisma User Creation**:
|
|
```typescript
|
|
await prisma.user.create({
|
|
data: {
|
|
id: user.id, // Use the Keycloak ID as primary ID
|
|
email: user.email,
|
|
password: tempPassword, // Temporary password (not used for auth)
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
},
|
|
});
|
|
```
|
|
|
|
**Important Notes**:
|
|
- The Prisma User `id` field uses the **Keycloak user ID** (UUID)
|
|
- The `password` field in Prisma is not used for authentication (Keycloak handles that)
|
|
- Users must exist in Prisma before they can use Courrier
|
|
|
|
### 3. Prisma Schema
|
|
|
|
**User Model** (`prisma/schema.prisma`):
|
|
```prisma
|
|
model User {
|
|
id String @id @default(uuid())
|
|
email String @unique
|
|
password String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
mailCredentials MailCredentials[] // One-to-many relationship
|
|
// ... other relations
|
|
}
|
|
```
|
|
|
|
**MailCredentials Model**:
|
|
```prisma
|
|
model MailCredentials {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
email String
|
|
password String? // Optional (for OAuth accounts)
|
|
host String
|
|
port Int
|
|
secure Boolean @default(true)
|
|
use_oauth Boolean @default(false)
|
|
refresh_token String?
|
|
access_token String?
|
|
token_expiry DateTime?
|
|
smtp_host String?
|
|
smtp_port Int?
|
|
smtp_secure Boolean? @default(false)
|
|
display_name String?
|
|
color String? @default("#0082c9")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, email]) // One email account per user
|
|
@@index([userId])
|
|
}
|
|
```
|
|
|
|
## Courrier's Role: Adding Email Accounts
|
|
|
|
### How Courrier Adds Email Accounts
|
|
|
|
**Location**: `app/api/courrier/account/route.ts` (POST method)
|
|
|
|
**Process**:
|
|
1. **Authentication Check**: Verifies user session exists
|
|
2. **User Existence Check**: Verifies user exists in Prisma database
|
|
3. **Connection Test**: Tests IMAP connection before saving
|
|
4. **Save Credentials**: Creates/updates `MailCredentials` record
|
|
|
|
**Key Code Flow**:
|
|
|
|
```typescript
|
|
// 1. Check if user exists in database
|
|
const userExistsInDB = await userExists(session.user.id);
|
|
if (!userExistsInDB) {
|
|
return NextResponse.json({
|
|
error: 'User not found in database',
|
|
details: `The user ID from your session (${session.user.id}) doesn't exist in the database.`
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// 2. Test connection
|
|
const testResult = await testEmailConnection(credentials);
|
|
if (!testResult.imap) {
|
|
return NextResponse.json({
|
|
error: `Connection test failed: ${testResult.error}`
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// 3. Save credentials
|
|
await saveUserEmailCredentials(session.user.id, email, credentials);
|
|
```
|
|
|
|
### Saving Email Credentials
|
|
|
|
**Location**: `lib/services/email-service.ts` → `saveUserEmailCredentials()`
|
|
|
|
**Process**:
|
|
1. Prepares database credentials object (excluding OAuth tokens)
|
|
2. Uses `upsert` to create or update `MailCredentials`
|
|
3. Caches full credentials (including OAuth tokens) in Redis
|
|
|
|
**Key Code**:
|
|
```typescript
|
|
// Save to database using upsert
|
|
await prisma.mailCredentials.upsert({
|
|
where: {
|
|
// Finds existing record by userId + email
|
|
userId_email: {
|
|
userId: userId,
|
|
email: credentials.email
|
|
}
|
|
},
|
|
update: dbCredentials,
|
|
create: {
|
|
userId,
|
|
...dbCredentials
|
|
}
|
|
});
|
|
|
|
// Cache full credentials (including OAuth) in Redis
|
|
await cacheEmailCredentials(userId, accountId, fullCreds);
|
|
```
|
|
|
|
**Important Notes**:
|
|
- OAuth tokens (access_token, refresh_token) are stored in **Redis only**, not in Prisma
|
|
- The Prisma `MailCredentials` table stores IMAP/SMTP settings
|
|
- The `password` field is optional (for OAuth accounts like Microsoft)
|
|
|
|
### Microsoft OAuth Flow
|
|
|
|
**Location**: `app/api/courrier/microsoft/callback/route.ts`
|
|
|
|
For Microsoft accounts, the flow is:
|
|
1. User authorizes via Microsoft OAuth
|
|
2. Access token and refresh token are obtained
|
|
3. Credentials are saved with `use_oauth: true`
|
|
4. OAuth tokens are cached in Redis (not in Prisma)
|
|
|
|
## Data Flow Diagram
|
|
|
|
```
|
|
┌─────────────┐
|
|
│ Keycloak │ ← Primary user creation
|
|
└──────┬──────┘
|
|
│
|
|
│ Sync
|
|
↓
|
|
┌─────────────┐
|
|
│ Prisma │ ← User record created
|
|
│ User │
|
|
└──────┬──────┘
|
|
│
|
|
│ User adds email account
|
|
↓
|
|
┌─────────────┐
|
|
│ Prisma │ ← MailCredentials created
|
|
│MailCredentials│
|
|
└──────┬──────┘
|
|
│
|
|
│ OAuth tokens (if applicable)
|
|
↓
|
|
┌─────────────┐
|
|
│ Redis │ ← OAuth tokens cached
|
|
└─────────────┘
|
|
```
|
|
|
|
## Key Files Reference
|
|
|
|
### User Creation
|
|
- `app/api/users/route.ts` - Creates users in Keycloak
|
|
- `scripts/sync-users.ts` - Syncs users from Keycloak to Prisma
|
|
- `app/api/sync-users/route.ts` - API endpoint for syncing users
|
|
|
|
### Courrier Email Management
|
|
- `app/api/courrier/account/route.ts` - Add/update/delete email accounts
|
|
- `lib/services/email-service.ts` - Core email service functions
|
|
- `saveUserEmailCredentials()` - Saves email credentials to Prisma
|
|
- `getUserEmailCredentials()` - Retrieves credentials from Prisma
|
|
- `testEmailConnection()` - Tests IMAP/SMTP connection
|
|
|
|
### Database Schema
|
|
- `prisma/schema.prisma` - Prisma schema definitions
|
|
- `lib/prisma.ts` - Prisma client instance
|
|
|
|
### Authentication
|
|
- `app/api/auth/options.ts` - NextAuth configuration
|
|
- `lib/auth.ts` - Authentication helpers
|
|
|
|
## Auto-Creation of Users
|
|
|
|
**As of recent updates**, Courrier now automatically creates User records in Prisma if they don't exist when:
|
|
- Adding an email account (`/api/courrier/account` POST)
|
|
- Checking session status (`/api/courrier/session` GET)
|
|
|
|
This handles cases where:
|
|
- The database was reset/lost but users still exist in Keycloak
|
|
- Users were created in Keycloak but never synced to Prisma
|
|
|
|
The auto-creation uses session data from Keycloak to populate:
|
|
- `id`: Keycloak user ID (UUID)
|
|
- `email`: User's email from session
|
|
- `password`: Temporary random password (not used for auth, Keycloak handles authentication)
|
|
|
|
## Common Issues & Solutions
|
|
|
|
### Issue: "User not found in database" when adding email account
|
|
|
|
**Cause**: User exists in Keycloak but not in Prisma database
|
|
|
|
**Solution**:
|
|
- **Automatic**: The system now auto-creates users when needed
|
|
- **Manual**: Run the sync script to create users in Prisma:
|
|
```bash
|
|
npm run sync-users
|
|
# or
|
|
node scripts/sync-users.js
|
|
```
|
|
|
|
### Issue: Email credentials not saving
|
|
|
|
**Check**:
|
|
1. User exists in Prisma: `prisma.user.findUnique({ where: { id: userId } })`
|
|
2. Connection test passes before saving
|
|
3. Unique constraint `[userId, email]` is not violated
|
|
|
|
### Issue: OAuth tokens not persisting
|
|
|
|
**Note**: OAuth tokens are stored in Redis, not Prisma. Check:
|
|
- Redis connection and TTL settings
|
|
- Redis cache functions in `lib/redis.ts`
|
|
|
|
## Summary
|
|
|
|
1. **Users are created in Keycloak first** (via `app/api/users/route.ts`)
|
|
2. **Users are synced to Prisma** (via sync scripts or API)
|
|
3. **Courrier adds email accounts** by creating `MailCredentials` records linked to existing Users
|
|
4. **OAuth tokens are cached in Redis**, not stored in Prisma
|
|
5. **Users must exist in Prisma** before they can add email accounts via Courrier
|
|
|
|
Courrier is a **credentials management system** for existing users, not a user creation system.
|
|
|