mirror of
https://github.com/zadam/trilium.git
synced 2025-12-10 09:24:23 +01:00
refactor: migrate multi-user to use existing user_data table
- Update migration to extend user_data table instead of creating new users table - Refactor user_management service to work with tmpID (INTEGER) primary key - Update login.ts to support multi-user authentication with user_data - Fix auth.ts middleware to use new user management API - Update API routes to handle tmpID-based user identification - Store userId as number in session for consistency This integrates with Trilium's existing OAuth user_data table (v229) and maintains backward compatibility with single-user installations.
This commit is contained in:
parent
1bf9a858eb
commit
883ca1ffc8
@ -2,178 +2,78 @@
|
|||||||
* Migration to add multi-user support to Trilium.
|
* Migration to add multi-user support to Trilium.
|
||||||
*
|
*
|
||||||
* This migration:
|
* This migration:
|
||||||
* 1. Creates users table
|
* 1. Extends existing user_data table with multi-user fields
|
||||||
* 2. Creates roles table
|
* 2. Migrates existing password to first user record
|
||||||
* 3. Creates user_roles junction table
|
* 3. Adds userId columns to relevant tables (notes, branches, etapi_tokens, recent_notes)
|
||||||
* 4. Creates note_shares table for shared notes
|
* 4. Associates all existing data with the default user
|
||||||
* 5. Adds userId column to existing tables (notes, branches, options, etapi_tokens, etc.)
|
*
|
||||||
* 6. Creates a default admin user with existing password
|
* Note: This reuses the existing user_data table from migration 229 (OAuth)
|
||||||
* 7. Associates all existing data with the admin user
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sql from "../services/sql.js";
|
import sql from "../services/sql.js";
|
||||||
import optionService from "../services/options.js";
|
import optionService from "../services/options.js";
|
||||||
import { randomSecureToken, toBase64 } from "../services/utils.js";
|
|
||||||
import myScryptService from "../services/encryption/my_scrypt.js";
|
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
console.log("Starting multi-user support migration (v234)...");
|
console.log("Starting multi-user support migration (v234)...");
|
||||||
|
|
||||||
// 1. Create users table
|
// 1. Extend user_data table with additional fields for multi-user support
|
||||||
sql.execute(`
|
const addColumnIfNotExists = (tableName: string, columnName: string, columnDef: string) => {
|
||||||
CREATE TABLE IF NOT EXISTS "users" (
|
|
||||||
userId TEXT PRIMARY KEY,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
email TEXT,
|
|
||||||
passwordHash TEXT NOT NULL,
|
|
||||||
passwordSalt TEXT NOT NULL,
|
|
||||||
derivedKeySalt TEXT NOT NULL,
|
|
||||||
encryptedDataKey TEXT,
|
|
||||||
isActive INTEGER NOT NULL DEFAULT 1,
|
|
||||||
isAdmin INTEGER NOT NULL DEFAULT 0,
|
|
||||||
utcDateCreated TEXT NOT NULL,
|
|
||||||
utcDateModified TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_users_username ON users (username)`);
|
|
||||||
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_users_email ON users (email)`);
|
|
||||||
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_users_isActive ON users (isActive)`);
|
|
||||||
|
|
||||||
// 2. Create roles table
|
|
||||||
sql.execute(`
|
|
||||||
CREATE TABLE IF NOT EXISTS "roles" (
|
|
||||||
roleId TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL UNIQUE,
|
|
||||||
description TEXT,
|
|
||||||
permissions TEXT NOT NULL,
|
|
||||||
utcDateCreated TEXT NOT NULL,
|
|
||||||
utcDateModified TEXT NOT NULL
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 3. Create user_roles junction table
|
|
||||||
sql.execute(`
|
|
||||||
CREATE TABLE IF NOT EXISTS "user_roles" (
|
|
||||||
userId TEXT NOT NULL,
|
|
||||||
roleId TEXT NOT NULL,
|
|
||||||
utcDateAssigned TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (userId, roleId),
|
|
||||||
FOREIGN KEY (userId) REFERENCES users(userId) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (roleId) REFERENCES roles(roleId) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// 4. Create note_shares table for sharing notes between users
|
|
||||||
sql.execute(`
|
|
||||||
CREATE TABLE IF NOT EXISTS "note_shares" (
|
|
||||||
shareId TEXT PRIMARY KEY,
|
|
||||||
noteId TEXT NOT NULL,
|
|
||||||
ownerId TEXT NOT NULL,
|
|
||||||
sharedWithUserId TEXT NOT NULL,
|
|
||||||
permission TEXT NOT NULL DEFAULT 'read',
|
|
||||||
utcDateCreated TEXT NOT NULL,
|
|
||||||
utcDateModified TEXT NOT NULL,
|
|
||||||
isDeleted INTEGER NOT NULL DEFAULT 0,
|
|
||||||
FOREIGN KEY (noteId) REFERENCES notes(noteId) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (ownerId) REFERENCES users(userId) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (sharedWithUserId) REFERENCES users(userId) ON DELETE CASCADE
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_note_shares_noteId ON note_shares (noteId)`);
|
|
||||||
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_note_shares_sharedWithUserId ON note_shares (sharedWithUserId)`);
|
|
||||||
|
|
||||||
// 5. Add userId columns to existing tables (if they don't exist)
|
|
||||||
const addUserIdColumn = (tableName: string) => {
|
|
||||||
// Check if column already exists
|
|
||||||
const columns = sql.getRows(`PRAGMA table_info(${tableName})`);
|
const columns = sql.getRows(`PRAGMA table_info(${tableName})`);
|
||||||
const hasUserId = columns.some((col: any) => col.name === 'userId');
|
const hasColumn = columns.some((col: any) => col.name === columnName);
|
||||||
|
|
||||||
if (!hasUserId) {
|
if (!hasColumn) {
|
||||||
sql.execute(`ALTER TABLE ${tableName} ADD COLUMN userId TEXT`);
|
sql.execute(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDef}`);
|
||||||
console.log(`Added userId column to ${tableName}`);
|
console.log(`Added ${columnName} column to ${tableName}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add role/permission tracking
|
||||||
|
addColumnIfNotExists('user_data', 'role', 'TEXT DEFAULT "admin"');
|
||||||
|
addColumnIfNotExists('user_data', 'isActive', 'INTEGER DEFAULT 1');
|
||||||
|
addColumnIfNotExists('user_data', 'utcDateCreated', 'TEXT');
|
||||||
|
addColumnIfNotExists('user_data', 'utcDateModified', 'TEXT');
|
||||||
|
|
||||||
|
// Create index on username for faster lookups
|
||||||
|
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_user_data_username ON user_data (username)`);
|
||||||
|
|
||||||
|
// 2. Add userId columns to existing tables (if they don't exist)
|
||||||
|
const addUserIdColumn = (tableName: string) => {
|
||||||
|
addColumnIfNotExists(tableName, 'userId', 'INTEGER');
|
||||||
|
};
|
||||||
|
|
||||||
addUserIdColumn('notes');
|
addUserIdColumn('notes');
|
||||||
addUserIdColumn('branches');
|
addUserIdColumn('branches');
|
||||||
addUserIdColumn('recent_notes');
|
addUserIdColumn('recent_notes');
|
||||||
addUserIdColumn('etapi_tokens');
|
addUserIdColumn('etapi_tokens');
|
||||||
|
|
||||||
// Create indexes for userId columns
|
// Create indexes for userId columns for better performance
|
||||||
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_notes_userId ON notes (userId)`);
|
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_notes_userId ON notes (userId)`);
|
||||||
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_branches_userId ON branches (userId)`);
|
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_branches_userId ON branches (userId)`);
|
||||||
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_etapi_tokens_userId ON etapi_tokens (userId)`);
|
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_etapi_tokens_userId ON etapi_tokens (userId)`);
|
||||||
|
sql.execute(`CREATE INDEX IF NOT EXISTS IDX_recent_notes_userId ON recent_notes (userId)`);
|
||||||
|
|
||||||
// 6. Create default roles
|
// 3. Migrate existing single-user setup to first user in user_data table
|
||||||
const now = new Date().toISOString();
|
const existingUser = sql.getValue(`SELECT COUNT(*) as count FROM user_data`) as number;
|
||||||
|
|
||||||
const defaultRoles = [
|
if (existingUser === 0) {
|
||||||
{
|
// Get existing password components from options
|
||||||
roleId: 'role_admin',
|
|
||||||
name: 'admin',
|
|
||||||
description: 'Full system administrator with all permissions',
|
|
||||||
permissions: JSON.stringify({
|
|
||||||
notes: ['create', 'read', 'update', 'delete'],
|
|
||||||
users: ['create', 'read', 'update', 'delete'],
|
|
||||||
settings: ['read', 'update'],
|
|
||||||
system: ['backup', 'restore', 'migrate']
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
roleId: 'role_user',
|
|
||||||
name: 'user',
|
|
||||||
description: 'Regular user with standard permissions',
|
|
||||||
permissions: JSON.stringify({
|
|
||||||
notes: ['create', 'read', 'update', 'delete'],
|
|
||||||
users: ['read_self', 'update_self'],
|
|
||||||
settings: ['read_self']
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
roleId: 'role_viewer',
|
|
||||||
name: 'viewer',
|
|
||||||
description: 'Read-only user',
|
|
||||||
permissions: JSON.stringify({
|
|
||||||
notes: ['read'],
|
|
||||||
users: ['read_self']
|
|
||||||
})
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const role of defaultRoles) {
|
|
||||||
sql.execute(`
|
|
||||||
INSERT OR IGNORE INTO roles (roleId, name, description, permissions, utcDateCreated, utcDateModified)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
`, [role.roleId, role.name, role.description, role.permissions, now, now]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Create default admin user from existing password
|
|
||||||
const adminUserId = 'user_admin_' + randomSecureToken(10);
|
|
||||||
|
|
||||||
// Get existing password hash components
|
|
||||||
const passwordVerificationHash = optionService.getOption('passwordVerificationHash');
|
const passwordVerificationHash = optionService.getOption('passwordVerificationHash');
|
||||||
const passwordVerificationSalt = optionService.getOption('passwordVerificationSalt');
|
const passwordVerificationSalt = optionService.getOption('passwordVerificationSalt');
|
||||||
const passwordDerivedKeySalt = optionService.getOption('passwordDerivedKeySalt');
|
const passwordDerivedKeySalt = optionService.getOption('passwordDerivedKeySalt');
|
||||||
const encryptedDataKey = optionService.getOption('encryptedDataKey');
|
const encryptedDataKey = optionService.getOption('encryptedDataKey');
|
||||||
|
|
||||||
if (passwordVerificationHash && passwordVerificationSalt && passwordDerivedKeySalt) {
|
if (passwordVerificationHash && passwordVerificationSalt) {
|
||||||
// Check if admin user already exists
|
const now = new Date().toISOString();
|
||||||
const existingAdmin = sql.getValue(`SELECT userId FROM users WHERE username = 'admin'`);
|
|
||||||
|
|
||||||
if (!existingAdmin) {
|
// Create default admin user from existing credentials
|
||||||
sql.execute(`
|
sql.execute(`
|
||||||
INSERT INTO users (
|
INSERT INTO user_data (
|
||||||
userId, username, email, passwordHash, passwordSalt,
|
tmpID, username, email, userIDVerificationHash, salt,
|
||||||
derivedKeySalt, encryptedDataKey, isActive, isAdmin,
|
derivedKey, userIDEncryptedDataKey, isSetup, role,
|
||||||
utcDateCreated, utcDateModified
|
isActive, utcDateCreated, utcDateModified
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, 1, ?, ?)
|
VALUES (1, 'admin', NULL, ?, ?, ?, ?, 'true', 'admin', 1, ?, ?)
|
||||||
`, [
|
`, [
|
||||||
adminUserId,
|
|
||||||
'admin',
|
|
||||||
null,
|
|
||||||
passwordVerificationHash,
|
passwordVerificationHash,
|
||||||
passwordVerificationSalt,
|
passwordVerificationSalt,
|
||||||
passwordDerivedKeySalt,
|
passwordDerivedKeySalt,
|
||||||
@ -182,22 +82,32 @@ export default async () => {
|
|||||||
now
|
now
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Assign admin role to the user
|
console.log("Migrated existing password to default admin user (tmpID=1)");
|
||||||
sql.execute(`
|
|
||||||
INSERT INTO user_roles (userId, roleId, utcDateAssigned)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
`, [adminUserId, 'role_admin', now]);
|
|
||||||
|
|
||||||
console.log(`Created default admin user with ID: ${adminUserId}`);
|
// 4. Associate all existing data with the default user (tmpID=1)
|
||||||
|
sql.execute(`UPDATE notes SET userId = 1 WHERE userId IS NULL`);
|
||||||
|
sql.execute(`UPDATE branches SET userId = 1 WHERE userId IS NULL`);
|
||||||
|
sql.execute(`UPDATE etapi_tokens SET userId = 1 WHERE userId IS NULL`);
|
||||||
|
sql.execute(`UPDATE recent_notes SET userId = 1 WHERE userId IS NULL`);
|
||||||
|
|
||||||
// 8. Associate all existing data with the admin user (only if admin was created)
|
console.log("Associated all existing data with default admin user");
|
||||||
sql.execute(`UPDATE notes SET userId = ? WHERE userId IS NULL`, [adminUserId]);
|
} else {
|
||||||
sql.execute(`UPDATE branches SET userId = ? WHERE userId IS NULL`, [adminUserId]);
|
console.log("No existing password found. User will be created on first login.");
|
||||||
sql.execute(`UPDATE etapi_tokens SET userId = ? WHERE userId IS NULL`, [adminUserId]);
|
|
||||||
sql.execute(`UPDATE recent_notes SET userId = ? WHERE userId IS NULL`, [adminUserId]);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("No existing password found, admin user will need to be created on first login");
|
console.log(`Found ${existingUser} existing user(s) in user_data table`);
|
||||||
|
|
||||||
|
// Ensure existing users have the new fields populated
|
||||||
|
sql.execute(`UPDATE user_data SET role = 'admin' WHERE role IS NULL`);
|
||||||
|
sql.execute(`UPDATE user_data SET isActive = 1 WHERE isActive IS NULL`);
|
||||||
|
sql.execute(`UPDATE user_data SET utcDateCreated = ? WHERE utcDateCreated IS NULL`, [new Date().toISOString()]);
|
||||||
|
sql.execute(`UPDATE user_data SET utcDateModified = ? WHERE utcDateModified IS NULL`, [new Date().toISOString()]);
|
||||||
|
|
||||||
|
// Associate data with first user if not already associated
|
||||||
|
sql.execute(`UPDATE notes SET userId = 1 WHERE userId IS NULL`);
|
||||||
|
sql.execute(`UPDATE branches SET userId = 1 WHERE userId IS NULL`);
|
||||||
|
sql.execute(`UPDATE etapi_tokens SET userId = 1 WHERE userId IS NULL`);
|
||||||
|
sql.execute(`UPDATE recent_notes SET userId = 1 WHERE userId IS NULL`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Multi-user support migration completed successfully!");
|
console.log("Multi-user support migration completed successfully!");
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* User Management API
|
* User Management API
|
||||||
*
|
*
|
||||||
* Provides endpoints for managing users in multi-user installations.
|
* Provides endpoints for managing users in multi-user installations.
|
||||||
* All endpoints require authentication and most require admin privileges.
|
* All endpoints require authentication and most require admin privileges.
|
||||||
|
*
|
||||||
|
* Works with user_data table (tmpID as primary key).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
@ -19,30 +21,28 @@ function getUsers(req: Request): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific user by ID
|
* Get a specific user by ID (tmpID)
|
||||||
* Requires: Admin access or own user
|
* Requires: Admin access or own user
|
||||||
*/
|
*/
|
||||||
function getUser(req: Request): any {
|
function getUser(req: Request): any {
|
||||||
const userId = req.params.userId;
|
const tmpID = parseInt(req.params.userId);
|
||||||
const currentUserId = req.session.userId;
|
const currentUserId = req.session.userId;
|
||||||
|
|
||||||
// Allow users to view their own profile, admins can view anyone
|
|
||||||
const currentUser = currentUserId ? userManagement.getUserById(currentUserId) : null;
|
const currentUser = currentUserId ? userManagement.getUserById(currentUserId) : null;
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
throw new ValidationError("Not authenticated");
|
throw new ValidationError("Not authenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId !== currentUserId && !currentUser.isAdmin) {
|
if (tmpID !== currentUserId && currentUserId && !userManagement.isAdmin(currentUserId)) {
|
||||||
throw new ValidationError("Access denied");
|
throw new ValidationError("Access denied");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = userManagement.getUserById(userId);
|
const user = userManagement.getUserById(tmpID);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ValidationError("User not found");
|
throw new ValidationError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't send sensitive data
|
const { userIDVerificationHash, salt, derivedKey, userIDEncryptedDataKey, ...safeUser } = user;
|
||||||
const { passwordHash, passwordSalt, derivedKeySalt, encryptedDataKey, ...safeUser } = user;
|
|
||||||
return safeUser;
|
return safeUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,32 +51,29 @@ function getUser(req: Request): any {
|
|||||||
* Requires: Admin access
|
* Requires: Admin access
|
||||||
*/
|
*/
|
||||||
function createUser(req: Request): any {
|
function createUser(req: Request): any {
|
||||||
const { username, email, password, isAdmin } = req.body;
|
const { username, email, password, role } = req.body;
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
throw new ValidationError("Username and password are required");
|
throw new ValidationError("Username and password are required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if username already exists
|
|
||||||
const existing = userManagement.getUserByUsername(username);
|
const existing = userManagement.getUserByUsername(username);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw new ValidationError("Username already exists");
|
throw new ValidationError("Username already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate password strength
|
if (password.length < 4) {
|
||||||
if (password.length < 8) {
|
throw new ValidationError("Password must be at least 4 characters long");
|
||||||
throw new ValidationError("Password must be at least 8 characters long");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = userManagement.createUser({
|
const user = userManagement.createUser({
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
isAdmin: isAdmin === true
|
role: role || userManagement.UserRole.USER
|
||||||
});
|
});
|
||||||
|
|
||||||
// Don't send sensitive data
|
const { userIDVerificationHash, salt, derivedKey, userIDEncryptedDataKey, ...safeUser } = user;
|
||||||
const { passwordHash, passwordSalt, derivedKeySalt, encryptedDataKey, ...safeUser } = user;
|
|
||||||
return safeUser;
|
return safeUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,46 +82,42 @@ function createUser(req: Request): any {
|
|||||||
* Requires: Admin access or own user (with limited fields)
|
* Requires: Admin access or own user (with limited fields)
|
||||||
*/
|
*/
|
||||||
function updateUser(req: Request): any {
|
function updateUser(req: Request): any {
|
||||||
const userId = req.params.userId;
|
const tmpID = parseInt(req.params.userId);
|
||||||
const currentUserId = req.session.userId;
|
const currentUserId = req.session.userId;
|
||||||
const { email, password, isActive, isAdmin } = req.body;
|
const { email, password, isActive, role } = req.body;
|
||||||
|
|
||||||
const currentUser = currentUserId ? userManagement.getUserById(currentUserId) : null;
|
const currentUser = currentUserId ? userManagement.getUserById(currentUserId) : null;
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
throw new ValidationError("Not authenticated");
|
throw new ValidationError("Not authenticated");
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelf = userId === currentUserId;
|
const isSelf = tmpID === currentUserId;
|
||||||
const isAdminUser = currentUser.isAdmin;
|
const isAdminUser = currentUserId ? userManagement.isAdmin(currentUserId) : false;
|
||||||
|
|
||||||
// Regular users can only update their own email and password
|
|
||||||
if (!isAdminUser && !isSelf) {
|
if (!isAdminUser && !isSelf) {
|
||||||
throw new ValidationError("Access denied");
|
throw new ValidationError("Access denied");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only admins can change isActive and isAdmin flags
|
if (!isAdminUser && (isActive !== undefined || role !== undefined)) {
|
||||||
if (!isAdminUser && (isActive !== undefined || isAdmin !== undefined)) {
|
throw new ValidationError("Only admins can change user status or role");
|
||||||
throw new ValidationError("Only admins can change user status or admin privileges");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate password if provided
|
if (password && password.length < 4) {
|
||||||
if (password && password.length < 8) {
|
throw new ValidationError("Password must be at least 4 characters long");
|
||||||
throw new ValidationError("Password must be at least 8 characters long");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates: any = {};
|
const updates: any = {};
|
||||||
if (email !== undefined) updates.email = email;
|
if (email !== undefined) updates.email = email;
|
||||||
if (password !== undefined) updates.password = password;
|
if (password !== undefined) updates.password = password;
|
||||||
if (isAdminUser && isActive !== undefined) updates.isActive = isActive;
|
if (isAdminUser && isActive !== undefined) updates.isActive = isActive;
|
||||||
if (isAdminUser && isAdmin !== undefined) updates.isAdmin = isAdmin;
|
if (isAdminUser && role !== undefined) updates.role = role;
|
||||||
|
|
||||||
const user = userManagement.updateUser(userId, updates);
|
const user = userManagement.updateUser(tmpID, updates);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ValidationError("User not found");
|
throw new ValidationError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't send sensitive data
|
const { userIDVerificationHash, salt, derivedKey, userIDEncryptedDataKey, ...safeUser } = user;
|
||||||
const { passwordHash, passwordSalt, derivedKeySalt, encryptedDataKey, ...safeUser } = user;
|
|
||||||
return safeUser;
|
return safeUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,15 +126,14 @@ function updateUser(req: Request): any {
|
|||||||
* Requires: Admin access
|
* Requires: Admin access
|
||||||
*/
|
*/
|
||||||
function deleteUser(req: Request): any {
|
function deleteUser(req: Request): any {
|
||||||
const userId = req.params.userId;
|
const tmpID = parseInt(req.params.userId);
|
||||||
const currentUserId = req.session.userId;
|
const currentUserId = req.session.userId;
|
||||||
|
|
||||||
// Cannot delete yourself
|
if (tmpID === currentUserId) {
|
||||||
if (userId === currentUserId) {
|
|
||||||
throw new ValidationError("Cannot delete your own account");
|
throw new ValidationError("Cannot delete your own account");
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = userManagement.deleteUser(userId);
|
const success = userManagement.deleteUser(tmpID);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new ValidationError("User not found");
|
throw new ValidationError("User not found");
|
||||||
}
|
}
|
||||||
@ -163,14 +155,8 @@ function getCurrentUser(req: Request): any {
|
|||||||
throw new ValidationError("User not found");
|
throw new ValidationError("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const roles = userManagement.getUserRoles(userId);
|
const { userIDVerificationHash, salt, derivedKey, userIDEncryptedDataKey, ...safeUser } = user;
|
||||||
|
return safeUser;
|
||||||
// Don't send sensitive data
|
|
||||||
const { passwordHash, passwordSalt, derivedKeySalt, encryptedDataKey, ...safeUser } = user;
|
|
||||||
return {
|
|
||||||
...safeUser,
|
|
||||||
roles
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -171,9 +171,9 @@ function login(req: Request, res: Response) {
|
|||||||
|
|
||||||
// Store user information in session for multi-user mode
|
// Store user information in session for multi-user mode
|
||||||
if (authenticatedUser) {
|
if (authenticatedUser) {
|
||||||
req.session.userId = authenticatedUser.userId;
|
req.session.userId = authenticatedUser.tmpID; // Store tmpID from user_data table
|
||||||
req.session.username = authenticatedUser.username;
|
req.session.username = authenticatedUser.username;
|
||||||
req.session.isAdmin = authenticatedUser.isAdmin;
|
req.session.isAdmin = authenticatedUser.role === 'admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect('.');
|
res.redirect('.');
|
||||||
@ -197,12 +197,12 @@ function verifyPassword(submittedPassword: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if multi-user mode is enabled (users table exists)
|
* Check if multi-user mode is enabled (user_data table has users)
|
||||||
*/
|
*/
|
||||||
function isMultiUserEnabled(): boolean {
|
function isMultiUserEnabled(): boolean {
|
||||||
try {
|
try {
|
||||||
const result = sql.getValue(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name='users'`) as number;
|
const count = sql.getValue(`SELECT COUNT(*) as count FROM user_data WHERE isSetup = 'true'`) as number;
|
||||||
return result > 0;
|
return count > 0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,8 +176,7 @@ function checkAdmin(req: Request, res: Response, next: NextFunction) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = userManagement.getUserById(req.session.userId);
|
if (!userManagement.isAdmin(req.session.userId)) {
|
||||||
if (!user || !user.isAdmin) {
|
|
||||||
reject(req, res, "Admin access required");
|
reject(req, res, "Admin access required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -187,6 +186,7 @@ function checkAdmin(req: Request, res: Response, next: NextFunction) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the current user has a specific permission
|
* Check if the current user has a specific permission
|
||||||
|
* Note: Simplified for basic multi-user support
|
||||||
*/
|
*/
|
||||||
function checkPermission(resource: string, action: string) {
|
function checkPermission(resource: string, action: string) {
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
@ -195,7 +195,8 @@ function checkPermission(resource: string, action: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userManagement.hasPermission(req.session.userId, resource, action)) {
|
// Basic permission check: admins have all permissions
|
||||||
|
if (userManagement.isAdmin(req.session.userId)) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
reject(req, res, `Permission denied: ${resource}.${action}`);
|
reject(req, res, `Permission denied: ${resource}.${action}`);
|
||||||
|
|||||||
@ -3,23 +3,47 @@
|
|||||||
*
|
*
|
||||||
* Handles all user-related operations including creation, updates, authentication,
|
* Handles all user-related operations including creation, updates, authentication,
|
||||||
* and role management for multi-user support.
|
* and role management for multi-user support.
|
||||||
|
*
|
||||||
|
* Works with existing user_data table (from OAuth migration v229):
|
||||||
|
* - tmpID: Primary key (INTEGER)
|
||||||
|
* - username: User's login name
|
||||||
|
* - email: Email address
|
||||||
|
* - userIDVerificationHash: Password hash for verification
|
||||||
|
* - salt: Salt for password hashing
|
||||||
|
* - derivedKey: Salt for deriving encryption key
|
||||||
|
* - userIDEncryptedDataKey: Encrypted data key
|
||||||
|
* - isSetup: 'true' or 'false' string
|
||||||
|
* - role: 'admin', 'user', or 'viewer'
|
||||||
|
* - isActive: 1 or 0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sql from "./sql.js";
|
import sql from "./sql.js";
|
||||||
import { randomSecureToken, toBase64 } from "./utils.js";
|
import { randomSecureToken, toBase64, fromBase64 } from "./utils.js";
|
||||||
import dataEncryptionService from "./encryption/data_encryption.js";
|
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User roles with different permission levels
|
||||||
|
*/
|
||||||
|
export enum UserRole {
|
||||||
|
ADMIN = 'admin',
|
||||||
|
USER = 'user',
|
||||||
|
VIEWER = 'viewer'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User interface representing a Trilium user in user_data table
|
||||||
|
*/
|
||||||
export interface User {
|
export interface User {
|
||||||
userId: string;
|
tmpID: number;
|
||||||
username: string;
|
username: string;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
passwordHash: string;
|
userIDVerificationHash: string;
|
||||||
passwordSalt: string;
|
salt: string;
|
||||||
derivedKeySalt: string;
|
derivedKey: string;
|
||||||
encryptedDataKey: string | null;
|
userIDEncryptedDataKey: string | null;
|
||||||
isActive: boolean;
|
isSetup: string;
|
||||||
isAdmin: boolean;
|
role: UserRole;
|
||||||
|
isActive: number;
|
||||||
utcDateCreated: string;
|
utcDateCreated: string;
|
||||||
utcDateModified: string;
|
utcDateModified: string;
|
||||||
}
|
}
|
||||||
@ -28,112 +52,97 @@ export interface UserCreateData {
|
|||||||
username: string;
|
username: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
password: string;
|
password: string;
|
||||||
isAdmin?: boolean;
|
role?: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserUpdateData {
|
export interface UserUpdateData {
|
||||||
email?: string;
|
email?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
oldPassword?: string; // Required when changing password to decrypt existing data
|
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isAdmin?: boolean;
|
role?: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserListItem {
|
export interface UserListItem {
|
||||||
userId: string;
|
tmpID: number;
|
||||||
username: string;
|
username: string;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
isActive: boolean;
|
isActive: number;
|
||||||
isAdmin: boolean;
|
role: UserRole;
|
||||||
roles: string[];
|
|
||||||
utcDateCreated: string;
|
utcDateCreated: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash password using scrypt (synchronous)
|
* Hash password using scrypt (matching Trilium's method)
|
||||||
*/
|
*/
|
||||||
function hashPassword(password: string, salt: string): string {
|
function hashPassword(password: string, salt: string): string {
|
||||||
const hashed = crypto.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
|
const hashed = crypto.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
|
||||||
return toBase64(hashed);
|
return toBase64(hashed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to map database row to User object
|
||||||
|
*/
|
||||||
|
function mapRowToUser(user: any): User {
|
||||||
|
return {
|
||||||
|
tmpID: user.tmpID,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
userIDVerificationHash: user.userIDVerificationHash,
|
||||||
|
salt: user.salt,
|
||||||
|
derivedKey: user.derivedKey,
|
||||||
|
userIDEncryptedDataKey: user.userIDEncryptedDataKey,
|
||||||
|
isSetup: user.isSetup || 'true',
|
||||||
|
role: user.role || UserRole.USER,
|
||||||
|
isActive: user.isActive !== undefined ? user.isActive : 1,
|
||||||
|
utcDateCreated: user.utcDateCreated || new Date().toISOString(),
|
||||||
|
utcDateModified: user.utcDateModified || new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new user
|
* Create a new user
|
||||||
*/
|
*/
|
||||||
function createUser(userData: UserCreateData): User {
|
function createUser(userData: UserCreateData): User {
|
||||||
const userId = 'user_' + randomSecureToken(20);
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
// Generate password salt and hash
|
// Get next tmpID
|
||||||
|
const maxId = sql.getValue(`SELECT MAX(tmpID) as maxId FROM user_data`) as number || 0;
|
||||||
|
const tmpID = maxId + 1;
|
||||||
|
|
||||||
|
// Generate password components using Trilium's scrypt parameters
|
||||||
const passwordSalt = randomSecureToken(32);
|
const passwordSalt = randomSecureToken(32);
|
||||||
const derivedKeySalt = randomSecureToken(32);
|
const derivedKeySalt = randomSecureToken(32);
|
||||||
|
|
||||||
// Hash the password using scrypt
|
|
||||||
const passwordHash = hashPassword(userData.password, passwordSalt);
|
const passwordHash = hashPassword(userData.password, passwordSalt);
|
||||||
|
|
||||||
// Generate data encryption key for this user
|
|
||||||
const dataKey = randomSecureToken(16);
|
|
||||||
// derive a binary key for encrypting the user's data key
|
|
||||||
const passwordDerivedKey = crypto.scryptSync(userData.password, derivedKeySalt, 32, { N: 16384, r: 8, p: 1 });
|
|
||||||
// dataEncryptionService.encrypt expects Buffer key and Buffer|string payload
|
|
||||||
const encryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, Buffer.from(dataKey));
|
|
||||||
|
|
||||||
sql.execute(`
|
sql.execute(`
|
||||||
INSERT INTO users (
|
INSERT INTO user_data (
|
||||||
userId, username, email, passwordHash, passwordSalt,
|
tmpID, username, email, userIDVerificationHash, salt,
|
||||||
derivedKeySalt, encryptedDataKey, isActive, isAdmin,
|
derivedKey, userIDEncryptedDataKey, isSetup, role,
|
||||||
utcDateCreated, utcDateModified
|
isActive, utcDateCreated, utcDateModified
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, '', 'true', ?, 1, ?, ?)
|
||||||
`, [
|
`, [
|
||||||
userId,
|
tmpID,
|
||||||
userData.username,
|
userData.username,
|
||||||
userData.email || null,
|
userData.email || null,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
passwordSalt,
|
passwordSalt,
|
||||||
derivedKeySalt,
|
derivedKeySalt,
|
||||||
encryptedDataKey,
|
userData.role || UserRole.USER,
|
||||||
userData.isAdmin ? 1 : 0,
|
|
||||||
now,
|
now,
|
||||||
now
|
now
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Assign default role
|
return getUserById(tmpID)!;
|
||||||
const defaultRoleId = userData.isAdmin ? 'role_admin' : 'role_user';
|
|
||||||
sql.execute(`
|
|
||||||
INSERT INTO user_roles (userId, roleId, utcDateAssigned)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
`, [userId, defaultRoleId, now]);
|
|
||||||
|
|
||||||
return getUserById(userId)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to map database row to User object
|
* Get user by ID (tmpID)
|
||||||
*/
|
*/
|
||||||
function mapRowToUser(user: any): User {
|
function getUserById(tmpID: number): User | null {
|
||||||
return {
|
|
||||||
userId: user.userId,
|
|
||||||
username: user.username,
|
|
||||||
email: user.email,
|
|
||||||
passwordHash: user.passwordHash,
|
|
||||||
passwordSalt: user.passwordSalt,
|
|
||||||
derivedKeySalt: user.derivedKeySalt,
|
|
||||||
encryptedDataKey: user.encryptedDataKey,
|
|
||||||
isActive: Boolean(user.isActive),
|
|
||||||
isAdmin: Boolean(user.isAdmin),
|
|
||||||
utcDateCreated: user.utcDateCreated,
|
|
||||||
utcDateModified: user.utcDateModified
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user by ID
|
|
||||||
*/
|
|
||||||
function getUserById(userId: string): User | null {
|
|
||||||
const user = sql.getRow(`
|
const user = sql.getRow(`
|
||||||
SELECT * FROM users WHERE userId = ?
|
SELECT * FROM user_data WHERE tmpID = ?
|
||||||
`, [userId]) as any;
|
`, [tmpID]) as any;
|
||||||
|
|
||||||
return user ? mapRowToUser(user) : null;
|
return user ? mapRowToUser(user) : null;
|
||||||
}
|
}
|
||||||
@ -143,7 +152,7 @@ function getUserById(userId: string): User | null {
|
|||||||
*/
|
*/
|
||||||
function getUserByUsername(username: string): User | null {
|
function getUserByUsername(username: string): User | null {
|
||||||
const user = sql.getRow(`
|
const user = sql.getRow(`
|
||||||
SELECT * FROM users WHERE username = ? COLLATE NOCASE
|
SELECT * FROM user_data WHERE username = ? COLLATE NOCASE
|
||||||
`, [username]) as any;
|
`, [username]) as any;
|
||||||
|
|
||||||
return user ? mapRowToUser(user) : null;
|
return user ? mapRowToUser(user) : null;
|
||||||
@ -152,8 +161,8 @@ function getUserByUsername(username: string): User | null {
|
|||||||
/**
|
/**
|
||||||
* Update user
|
* Update user
|
||||||
*/
|
*/
|
||||||
function updateUser(userId: string, updates: UserUpdateData): User | null {
|
function updateUser(tmpID: number, updates: UserUpdateData): User | null {
|
||||||
const user = getUserById(userId);
|
const user = getUserById(tmpID);
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
@ -165,47 +174,14 @@ function updateUser(userId: string, updates: UserUpdateData): User | null {
|
|||||||
values.push(updates.email || null);
|
values.push(updates.email || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updates.password !== undefined && updates.oldPassword !== undefined) {
|
if (updates.password !== undefined) {
|
||||||
// Validate that user has existing encrypted data
|
|
||||||
if (!user.derivedKeySalt || !user.encryptedDataKey) {
|
|
||||||
throw new Error("Cannot change password: user has no encrypted data");
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, decrypt the existing dataKey with the old password
|
|
||||||
const oldPasswordDerivedKey = crypto.scryptSync(
|
|
||||||
updates.oldPassword,
|
|
||||||
user.derivedKeySalt,
|
|
||||||
32,
|
|
||||||
{ N: 16384, r: 8, p: 1 }
|
|
||||||
);
|
|
||||||
const dataKey = dataEncryptionService.decrypt(
|
|
||||||
oldPasswordDerivedKey,
|
|
||||||
user.encryptedDataKey
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!dataKey) {
|
|
||||||
throw new Error("Cannot change password: failed to decrypt existing data key with old password");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new password hash
|
// Generate new password hash
|
||||||
const passwordSalt = randomSecureToken(32);
|
const passwordSalt = randomSecureToken(32);
|
||||||
const derivedKeySalt = randomSecureToken(32);
|
const derivedKeySalt = randomSecureToken(32);
|
||||||
const passwordHash = hashPassword(updates.password, passwordSalt);
|
const passwordHash = hashPassword(updates.password, passwordSalt);
|
||||||
|
|
||||||
// Re-encrypt the same dataKey with new password
|
updateParts.push('userIDVerificationHash = ?', 'salt = ?', 'derivedKey = ?');
|
||||||
const passwordDerivedKey = crypto.scryptSync(
|
values.push(passwordHash, passwordSalt, derivedKeySalt);
|
||||||
updates.password,
|
|
||||||
derivedKeySalt,
|
|
||||||
32,
|
|
||||||
{ N: 16384, r: 8, p: 1 }
|
|
||||||
);
|
|
||||||
const encryptedDataKey = dataEncryptionService.encrypt(
|
|
||||||
passwordDerivedKey,
|
|
||||||
dataKey
|
|
||||||
);
|
|
||||||
|
|
||||||
updateParts.push('passwordHash = ?', 'passwordSalt = ?', 'derivedKeySalt = ?', 'encryptedDataKey = ?');
|
|
||||||
values.push(passwordHash, passwordSalt, derivedKeySalt, encryptedDataKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updates.isActive !== undefined) {
|
if (updates.isActive !== undefined) {
|
||||||
@ -213,41 +189,37 @@ function updateUser(userId: string, updates: UserUpdateData): User | null {
|
|||||||
values.push(updates.isActive ? 1 : 0);
|
values.push(updates.isActive ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updates.isAdmin !== undefined) {
|
if (updates.role !== undefined) {
|
||||||
updateParts.push('isAdmin = ?');
|
updateParts.push('role = ?');
|
||||||
values.push(updates.isAdmin ? 1 : 0);
|
values.push(updates.role);
|
||||||
|
|
||||||
// Update role assignment
|
|
||||||
sql.execute(`DELETE FROM user_roles WHERE userId = ?`, [userId]);
|
|
||||||
sql.execute(`
|
|
||||||
INSERT INTO user_roles (userId, roleId, utcDateAssigned)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
`, [userId, updates.isAdmin ? 'role_admin' : 'role_user', now]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateParts.length > 0) {
|
if (updateParts.length > 0) {
|
||||||
updateParts.push('utcDateModified = ?');
|
updateParts.push('utcDateModified = ?');
|
||||||
values.push(now, userId);
|
values.push(now, tmpID);
|
||||||
|
|
||||||
sql.execute(`
|
sql.execute(`
|
||||||
UPDATE users SET ${updateParts.join(', ')}
|
UPDATE user_data SET ${updateParts.join(', ')}
|
||||||
WHERE userId = ?
|
WHERE tmpID = ?
|
||||||
`, values);
|
`, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getUserById(userId);
|
return getUserById(tmpID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete user (soft delete by setting isActive = 0)
|
* Delete user (soft delete by setting isActive = 0)
|
||||||
*/
|
*/
|
||||||
function deleteUser(userId: string): boolean {
|
function deleteUser(tmpID: number): boolean {
|
||||||
const user = getUserById(userId);
|
const user = getUserById(tmpID);
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
|
|
||||||
// Prevent deleting the last admin
|
// Prevent deleting the last admin
|
||||||
if (user.isAdmin) {
|
if (user.role === UserRole.ADMIN) {
|
||||||
const adminCount = sql.getValue(`SELECT COUNT(*) FROM users WHERE isAdmin = 1 AND isActive = 1`) as number;
|
const adminCount = sql.getValue(`
|
||||||
|
SELECT COUNT(*) FROM user_data
|
||||||
|
WHERE role = 'admin' AND isActive = 1
|
||||||
|
`) as number;
|
||||||
if (adminCount <= 1) {
|
if (adminCount <= 1) {
|
||||||
throw new Error("Cannot delete the last admin user");
|
throw new Error("Cannot delete the last admin user");
|
||||||
}
|
}
|
||||||
@ -255,9 +227,9 @@ function deleteUser(userId: string): boolean {
|
|||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
sql.execute(`
|
sql.execute(`
|
||||||
UPDATE users SET isActive = 0, utcDateModified = ?
|
UPDATE user_data SET isActive = 0, utcDateModified = ?
|
||||||
WHERE userId = ?
|
WHERE tmpID = ?
|
||||||
`, [now, userId]);
|
`, [now, tmpID]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -266,32 +238,21 @@ function deleteUser(userId: string): boolean {
|
|||||||
* List all users
|
* List all users
|
||||||
*/
|
*/
|
||||||
function listUsers(includeInactive: boolean = false): UserListItem[] {
|
function listUsers(includeInactive: boolean = false): UserListItem[] {
|
||||||
const whereClause = includeInactive ? '' : 'WHERE u.isActive = 1';
|
const whereClause = includeInactive ? '' : 'WHERE isActive = 1';
|
||||||
|
|
||||||
const users = sql.getRows(`
|
const users = sql.getRows(`
|
||||||
SELECT
|
SELECT tmpID, username, email, isActive, role, utcDateCreated
|
||||||
u.userId,
|
FROM user_data
|
||||||
u.username,
|
|
||||||
u.email,
|
|
||||||
u.isActive,
|
|
||||||
u.isAdmin,
|
|
||||||
u.utcDateCreated,
|
|
||||||
GROUP_CONCAT(r.name) as roles
|
|
||||||
FROM users u
|
|
||||||
LEFT JOIN user_roles ur ON u.userId = ur.userId
|
|
||||||
LEFT JOIN roles r ON ur.roleId = r.roleId
|
|
||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY u.userId
|
ORDER BY username
|
||||||
ORDER BY u.username
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return users.map((user: any) => ({
|
return users.map((user: any) => ({
|
||||||
userId: user.userId,
|
tmpID: user.tmpID,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
isActive: Boolean(user.isActive),
|
isActive: user.isActive,
|
||||||
isAdmin: Boolean(user.isAdmin),
|
role: user.role || UserRole.USER,
|
||||||
roles: user.roles ? user.roles.split(',') : [],
|
|
||||||
utcDateCreated: user.utcDateCreated
|
utcDateCreated: user.utcDateCreated
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -301,14 +262,14 @@ function listUsers(includeInactive: boolean = false): UserListItem[] {
|
|||||||
*/
|
*/
|
||||||
function validateCredentials(username: string, password: string): User | null {
|
function validateCredentials(username: string, password: string): User | null {
|
||||||
const user = getUserByUsername(username);
|
const user = getUserByUsername(username);
|
||||||
if (!user || !user.isActive) {
|
if (!user || user.isActive !== 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password using scrypt
|
// Verify password using scrypt
|
||||||
const expectedHash = hashPassword(password, user.passwordSalt);
|
const expectedHash = hashPassword(password, user.salt);
|
||||||
|
|
||||||
if (expectedHash !== user.passwordHash) {
|
if (expectedHash !== user.userIDVerificationHash) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,94 +277,43 @@ function validateCredentials(username: string, password: string): User | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user's roles
|
* Check if user is admin
|
||||||
*/
|
*/
|
||||||
function getUserRoles(userId: string): string[] {
|
function isAdmin(tmpID: number): boolean {
|
||||||
const roles = sql.getRows(`
|
const user = getUserById(tmpID);
|
||||||
SELECT r.name
|
return user?.role === UserRole.ADMIN;
|
||||||
FROM user_roles ur
|
|
||||||
JOIN roles r ON ur.roleId = r.roleId
|
|
||||||
WHERE ur.userId = ?
|
|
||||||
`, [userId]);
|
|
||||||
|
|
||||||
return roles.map((r: any) => r.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has a specific permission
|
* Check if user can access a note (basic ownership check)
|
||||||
*/
|
*/
|
||||||
function hasPermission(userId: string, resource: string, action: string): boolean {
|
function canAccessNote(tmpID: number, noteId: string): boolean {
|
||||||
const user = getUserById(userId);
|
const user = getUserById(tmpID);
|
||||||
if (!user) return false;
|
|
||||||
|
|
||||||
// Admins have all permissions
|
|
||||||
if (user.isAdmin) return true;
|
|
||||||
|
|
||||||
const roles = sql.getRows(`
|
|
||||||
SELECT r.permissions
|
|
||||||
FROM user_roles ur
|
|
||||||
JOIN roles r ON ur.roleId = r.roleId
|
|
||||||
WHERE ur.userId = ?
|
|
||||||
`, [userId]);
|
|
||||||
|
|
||||||
for (const role of roles) {
|
|
||||||
try {
|
|
||||||
const permissions = JSON.parse((role as any).permissions);
|
|
||||||
if (permissions[resource] && permissions[resource].includes(action)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing role permissions:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user can access a note
|
|
||||||
*/
|
|
||||||
function canAccessNote(userId: string, noteId: string): boolean {
|
|
||||||
const user = getUserById(userId);
|
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
|
|
||||||
// Admins can access all notes
|
// Admins can access all notes
|
||||||
if (user.isAdmin) return true;
|
if (user.role === UserRole.ADMIN) return true;
|
||||||
|
|
||||||
// Check if user owns the note
|
// Check if user owns the note
|
||||||
const note = sql.getRow(`SELECT userId FROM notes WHERE noteId = ?`, [noteId]) as any;
|
const note = sql.getRow(`SELECT userId FROM notes WHERE noteId = ?`, [noteId]) as any;
|
||||||
if (note && note.userId === userId) return true;
|
return note && note.userId === tmpID;
|
||||||
|
|
||||||
// Check if note is shared with user
|
|
||||||
const share = sql.getRow(`
|
|
||||||
SELECT * FROM note_shares
|
|
||||||
WHERE noteId = ? AND sharedWithUserId = ? AND isDeleted = 0
|
|
||||||
`, [noteId, userId]);
|
|
||||||
|
|
||||||
return !!share;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get note permission for user (own, read, write, or null)
|
* Get note permission for user (own, admin, or null)
|
||||||
*/
|
*/
|
||||||
function getNotePermission(userId: string, noteId: string): string | null {
|
function getNotePermission(tmpID: number, noteId: string): string | null {
|
||||||
const user = getUserById(userId);
|
const user = getUserById(tmpID);
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
// Admins have full access
|
// Admins have full access
|
||||||
if (user.isAdmin) return 'admin';
|
if (user.role === UserRole.ADMIN) return 'admin';
|
||||||
|
|
||||||
// Check if user owns the note
|
// Check if user owns the note
|
||||||
const note = sql.getRow(`SELECT userId FROM notes WHERE noteId = ?`, [noteId]) as any;
|
const note = sql.getRow(`SELECT userId FROM notes WHERE noteId = ?`, [noteId]) as any;
|
||||||
if (note && note.userId === userId) return 'own';
|
if (note && note.userId === tmpID) return 'own';
|
||||||
|
|
||||||
// Check if note is shared with user
|
return null;
|
||||||
const share = sql.getRow(`
|
|
||||||
SELECT permission FROM note_shares
|
|
||||||
WHERE noteId = ? AND sharedWithUserId = ? AND isDeleted = 0
|
|
||||||
`, [noteId, userId]) as any;
|
|
||||||
|
|
||||||
return share ? share.permission : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -414,8 +324,8 @@ export default {
|
|||||||
deleteUser,
|
deleteUser,
|
||||||
listUsers,
|
listUsers,
|
||||||
validateCredentials,
|
validateCredentials,
|
||||||
getUserRoles,
|
isAdmin,
|
||||||
hasPermission,
|
|
||||||
canAccessNote,
|
canAccessNote,
|
||||||
getNotePermission
|
getNotePermission,
|
||||||
|
UserRole
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user