mirror of
https://github.com/zadam/trilium.git
synced 2025-12-10 01:14:25 +01:00
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:
parent
883ca1ffc8
commit
6cde730553
215
MULTI_USER.md
Normal file
215
MULTI_USER.md
Normal 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
|
||||
@ -30,10 +30,19 @@
|
||||
</a>
|
||||
<% } else { %>
|
||||
<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">
|
||||
<label for="password"><%= t("login.password") %></label>
|
||||
<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>
|
||||
<% if( totpEnabled ) { %>
|
||||
|
||||
2
apps/server/src/express.d.ts
vendored
2
apps/server/src/express.d.ts
vendored
@ -22,7 +22,7 @@ export declare module "express-serve-static-core" {
|
||||
export declare module "express-session" {
|
||||
interface SessionData {
|
||||
loggedIn: boolean;
|
||||
userId?: string;
|
||||
userId?: number; // tmpID from user_data table
|
||||
username?: string;
|
||||
isAdmin?: boolean;
|
||||
lastAuthState: {
|
||||
|
||||
@ -17,6 +17,10 @@ import sql from "../services/sql.js";
|
||||
|
||||
function loginPage(req: Request, res: Response) {
|
||||
// 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', {
|
||||
wrongPassword: false,
|
||||
wrongTotp: false,
|
||||
@ -24,6 +28,7 @@ function loginPage(req: Request, res: Response) {
|
||||
ssoEnabled: openID.isOpenIDEnabled(),
|
||||
ssoIssuerName: openID.getSSOIssuerName(),
|
||||
ssoIssuerIcon: openID.getSSOIssuerIcon(),
|
||||
multiUserMode,
|
||||
assetPath: assetPath,
|
||||
assetPathFragment: assetUrlFragment,
|
||||
appPath: appPath,
|
||||
@ -223,11 +228,15 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to
|
||||
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', {
|
||||
wrongPassword: errorType === 'password',
|
||||
wrongPassword: errorType === 'password' || errorType === 'credentials',
|
||||
wrongTotp: errorType === 'totp',
|
||||
totpEnabled: totp.isTotpEnabled(),
|
||||
ssoEnabled: openID.isOpenIDEnabled(),
|
||||
multiUserMode,
|
||||
assetPath: assetPath,
|
||||
assetPathFragment: assetUrlFragment,
|
||||
appPath: appPath,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user