feat: complete multi-user implementation with authentication and documentation

- Update login flow to support multi-user mode with username field
- Fix session type definitions (userId as number/tmpID)
- Add comprehensive MULTI_USER.md documentation covering:
  * Architecture and database schema details
  * Setup instructions and API reference
  * Security implementation (scrypt parameters)
  * Backward compatibility with single-user mode
  * Future enhancements and limitations

All components now properly integrate with existing user_data table
from OAuth migration v229. Zero TypeScript errors.
This commit is contained in:
Somoru 2025-10-21 14:51:20 +05:30
parent 883ca1ffc8
commit 6cde730553
4 changed files with 236 additions and 3 deletions

215
MULTI_USER.md Normal file
View File

@ -0,0 +1,215 @@
# Multi-User Support for Trilium Notes
This document describes the multi-user functionality added to Trilium Notes.
## Overview
Trilium now supports multiple users with role-based access control. Each user has their own credentials and can be assigned different roles (Admin, User, or Viewer).
## Architecture
### Database Schema
Multi-user support extends the existing `user_data` table (introduced in migration v229 for OAuth):
**user_data table fields:**
- `tmpID`: INTEGER primary key
- `username`: User's login name
- `email`: Optional email address
- `userIDVerificationHash`: Password hash (scrypt)
- `salt`: Password salt
- `derivedKey`: Key derivation salt
- `userIDEncryptedDataKey`: Encrypted data key (currently unused)
- `isSetup`: 'true' or 'false' string
- `role`: 'admin', 'user', or 'viewer'
- `isActive`: 1 (active) or 0 (inactive)
- `utcDateCreated`: Creation timestamp
- `utcDateModified`: Last modification timestamp
### User Roles
- **Admin**: Full access to all notes and user management
- **User**: Can create, read, update, and delete their own notes
- **Viewer**: Read-only access to their notes
### Migration (v234)
The migration automatically:
1. Extends the `user_data` table with role and status fields
2. Adds `userId` columns to notes, branches, etapi_tokens, and recent_notes tables
3. Creates a default admin user from existing single-user credentials
4. Associates all existing data with the admin user
5. Maintains backward compatibility with single-user installations
## Setup
### For New Installations
On first login, set a password as usual. This creates the default admin user.
### For Existing Installations
When you upgrade, the migration runs automatically:
1. Your existing password becomes the admin user's password
2. Username defaults to "admin"
3. All your existing notes remain accessible
### Creating Additional Users
After migration, you can create additional users via the REST API:
```bash
# Create a new user (requires admin privileges)
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-H "Cookie: connect.sid=YOUR_SESSION_COOKIE" \
-d '{
"username": "newuser",
"email": "user@example.com",
"password": "securepassword",
"role": "user"
}'
```
## API Endpoints
All endpoints require authentication. Most require admin privileges.
### List Users
```
GET /api/users
Query params: includeInactive=true (optional)
Requires: Admin
```
### Get User
```
GET /api/users/:userId
Requires: Admin or own user
```
### Create User
```
POST /api/users
Body: { username, email?, password, role? }
Requires: Admin
```
### Update User
```
PUT /api/users/:userId
Body: { email?, password?, isActive?, role? }
Requires: Admin (or own user for email/password only)
```
### Delete User
```
DELETE /api/users/:userId
Requires: Admin
Note: Soft delete (sets isActive=0)
```
### Get Current User
```
GET /api/users/current
Requires: Authentication
```
### Check Username Availability
```
GET /api/users/check-username?username=testuser
Requires: Authentication
```
## Login
### Single-User Mode
If only one user exists, login works as before (password-only).
### Multi-User Mode
When multiple users exist:
1. Username field appears on login page
2. Enter username + password to authenticate
3. Session stores user ID and role
## Security
- Passwords are hashed using scrypt (N=16384, r=8, p=1)
- Each user has unique salt
- Sessions are maintained using express-session
- Users can only access their own notes (except admins)
## Backward Compatibility
- Single-user installations continue to work without changes
- No username field shown if only one user exists
- Existing password continues to work after migration
- All existing notes remain accessible
## Limitations
- No per-note sharing between users yet (planned for future)
- No user interface for user management (use API)
- Sync protocol not yet multi-user aware
- No user switching without logout
## Future Enhancements
1. **UI for User Management**: Add settings dialog for creating/managing users
2. **Note Sharing**: Implement per-note sharing with other users
3. **Sync Support**: Update sync protocol for multi-instance scenarios
4. **User Switching**: Allow switching users without logout
5. **Groups**: Add user groups for easier permission management
6. **Audit Log**: Track user actions for security
## Troubleshooting
### Can't log in after migration
- Try username "admin" with your existing password
- Check server logs for migration errors
### Want to reset admin password
1. Stop Trilium
2. Access document.db directly
3. Update the user_data table manually
4. Restart Trilium
### Want to disable multi-user
Not currently supported. Once migrated, single-user mode won't work if additional users exist.
## Technical Details
### Files Modified
- `apps/server/src/migrations/0234__multi_user_support.ts` - Migration
- `apps/server/src/services/user_management.ts` - User management service
- `apps/server/src/routes/api/users.ts` - REST API endpoints
- `apps/server/src/routes/login.ts` - Multi-user login logic
- `apps/server/src/services/auth.ts` - Authentication middleware
- `apps/server/src/express.d.ts` - Session type definitions
- `apps/server/src/assets/views/login.ejs` - Login page UI
### Testing
```bash
# Run tests
pnpm test
# Build
pnpm build
# Check TypeScript
pnpm --filter @triliumnext/server typecheck
```
## Contributing
When extending multi-user support:
1. Always test with both single-user and multi-user modes
2. Maintain backward compatibility
3. Update this documentation
4. Add tests for new functionality
## Support
For issues or questions:
- GitHub Issues: https://github.com/TriliumNext/Trilium/issues
- Discussions: https://github.com/orgs/TriliumNext/discussions

View File

@ -30,10 +30,19 @@
</a> </a>
<% } else { %> <% } else { %>
<form action="login" method="POST"> <form action="login" method="POST">
<% if (multiUserMode) { %>
<div class="form-group">
<label for="username"><%= t("login.username") || "Username" %></label>
<div class="controls">
<input id="username" name="username" placeholder="" class="form-control" type="text" autocomplete="username" autofocus>
</div>
</div>
<% } %>
<div class="form-group"> <div class="form-group">
<label for="password"><%= t("login.password") %></label> <label for="password"><%= t("login.password") %></label>
<div class="controls"> <div class="controls">
<input id="password" name="password" placeholder="" class="form-control" type="password" autocomplete="current-password" autofocus> <input id="password" name="password" placeholder="" class="form-control" type="password" autocomplete="current-password" <% if (!multiUserMode) { %>autofocus<% } %>>
</div> </div>
</div> </div>
<% if( totpEnabled ) { %> <% if( totpEnabled ) { %>

View File

@ -22,7 +22,7 @@ export declare module "express-serve-static-core" {
export declare module "express-session" { export declare module "express-session" {
interface SessionData { interface SessionData {
loggedIn: boolean; loggedIn: boolean;
userId?: string; userId?: number; // tmpID from user_data table
username?: string; username?: string;
isAdmin?: boolean; isAdmin?: boolean;
lastAuthState: { lastAuthState: {

View File

@ -17,6 +17,10 @@ import sql from "../services/sql.js";
function loginPage(req: Request, res: Response) { function loginPage(req: Request, res: Response) {
// Login page is triggered twice. Once here, and another time (see sendLoginError) if the password is failed. // Login page is triggered twice. Once here, and another time (see sendLoginError) if the password is failed.
// Check if multi-user mode is active
const userCount = isMultiUserEnabled() ? sql.getValue(`SELECT COUNT(*) FROM user_data`) as number : 0;
const multiUserMode = userCount > 1;
res.render('login', { res.render('login', {
wrongPassword: false, wrongPassword: false,
wrongTotp: false, wrongTotp: false,
@ -24,6 +28,7 @@ function loginPage(req: Request, res: Response) {
ssoEnabled: openID.isOpenIDEnabled(), ssoEnabled: openID.isOpenIDEnabled(),
ssoIssuerName: openID.getSSOIssuerName(), ssoIssuerName: openID.getSSOIssuerName(),
ssoIssuerIcon: openID.getSSOIssuerIcon(), ssoIssuerIcon: openID.getSSOIssuerIcon(),
multiUserMode,
assetPath: assetPath, assetPath: assetPath,
assetPathFragment: assetUrlFragment, assetPathFragment: assetUrlFragment,
appPath: appPath, appPath: appPath,
@ -223,11 +228,15 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to
log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`); log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
} }
const userCount = isMultiUserEnabled() ? sql.getValue(`SELECT COUNT(*) FROM user_data`) as number : 0;
const multiUserMode = userCount > 1;
res.status(401).render('login', { res.status(401).render('login', {
wrongPassword: errorType === 'password', wrongPassword: errorType === 'password' || errorType === 'credentials',
wrongTotp: errorType === 'totp', wrongTotp: errorType === 'totp',
totpEnabled: totp.isTotpEnabled(), totpEnabled: totp.isTotpEnabled(),
ssoEnabled: openID.isOpenIDEnabled(), ssoEnabled: openID.isOpenIDEnabled(),
multiUserMode,
assetPath: assetPath, assetPath: assetPath,
assetPathFragment: assetUrlFragment, assetPathFragment: assetUrlFragment,
appPath: appPath, appPath: appPath,