9.0 KiB
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:
- User is created in Keycloak via Admin API
- Roles are assigned to the user
- User may be created in external systems:
- Leantime (project management tool)
- Dolibarr (if user has "Mediation" or "Expression" roles)
Key Code:
// 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.tsorscripts/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:
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
idfield uses the Keycloak user ID (UUID) - The
passwordfield 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):
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:
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:
- Authentication Check: Verifies user session exists
- User Existence Check: Verifies user exists in Prisma database
- Connection Test: Tests IMAP connection before saving
- Save Credentials: Creates/updates
MailCredentialsrecord
Key Code Flow:
// 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:
- Prepares database credentials object (excluding OAuth tokens)
- Uses
upsertto create or updateMailCredentials - Caches full credentials (including OAuth tokens) in Redis
Key Code:
// 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
MailCredentialstable stores IMAP/SMTP settings - The
passwordfield is optional (for OAuth accounts like Microsoft)
Microsoft OAuth Flow
Location: app/api/courrier/microsoft/callback/route.ts
For Microsoft accounts, the flow is:
- User authorizes via Microsoft OAuth
- Access token and refresh token are obtained
- Credentials are saved with
use_oauth: true - 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 Keycloakscripts/sync-users.ts- Syncs users from Keycloak to Prismaapp/api/sync-users/route.ts- API endpoint for syncing users
Courrier Email Management
app/api/courrier/account/route.ts- Add/update/delete email accountslib/services/email-service.ts- Core email service functionssaveUserEmailCredentials()- Saves email credentials to PrismagetUserEmailCredentials()- Retrieves credentials from PrismatestEmailConnection()- Tests IMAP/SMTP connection
Database Schema
prisma/schema.prisma- Prisma schema definitionslib/prisma.ts- Prisma client instance
Authentication
app/api/auth/options.ts- NextAuth configurationlib/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/accountPOST) - Checking session status (
/api/courrier/sessionGET)
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 sessionpassword: 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:
npm run sync-users
# or
node scripts/sync-users.js
Issue: Email credentials not saving
Check:
- User exists in Prisma:
prisma.user.findUnique({ where: { id: userId } }) - Connection test passes before saving
- 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
- Users are created in Keycloak first (via
app/api/users/route.ts) - Users are synced to Prisma (via sync scripts or API)
- Courrier adds email accounts by creating
MailCredentialsrecords linked to existing Users - OAuth tokens are cached in Redis, not stored in Prisma
- 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.