tududi/backend/migrations/20260420000004-make-password-optional.js
Chris 7948f6552c
fix: make password_digest migration compatible with all schema versions (#1078)
* fix: replace 6-word limit with 150-character limit for project names

Replaces the word-based validation with character-based validation
as originally requested in #971. The 6-word limit was causing issues
with small words and separators being counted equally, and didn't
match the original requirement for a character limit.

Changes:
- Backend: Replace wordCount validator with len validator (1-150 chars)
- Frontend: Replace word count validation with character length check
- UI already has line-clamp-3 for display truncation

Fixes #998

* fix: make password_digest migration compatible with all schema versions

Fixes a critical bug where the make-password-optional migration would silently
fail when upgrading from v1.0.0 or running on fresh v1.1.0-dev installations.

The migration was trying to SELECT columns (ai_provider, openai_api_key,
ollama_base_url, ollama_model) that don't exist in the users table at that
point in the migration chain, causing the INSERT...SELECT to fail and leaving
password_digest as NOT NULL. This prevented OIDC auto-provisioning from
creating new users without passwords.

The fix dynamically detects which columns exist in the users table using
PRAGMA table_info and only selects columns that are guaranteed to exist.
Missing columns (AI-related fields) will receive their default values from
the new table schema.

Changes:
- Added dynamic column detection using PRAGMA table_info
- Only SELECT columns that exist in the current users table
- AI columns get default values if they don't exist yet
- Applied same fix to both up and down migrations
- Properly handle password/password_digest column name migration

Fixes #1075
2026-04-26 10:07:55 +03:00

273 lines
9.9 KiB
JavaScript

'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF;');
await queryInterface.sequelize.query('DROP TABLE IF EXISTS users_new;');
await queryInterface.sequelize.query(`
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255),
surname VARCHAR(255),
email VARCHAR(255) NOT NULL UNIQUE,
password_digest VARCHAR(255),
appearance VARCHAR(255) NOT NULL DEFAULT 'light',
language VARCHAR(255) NOT NULL DEFAULT 'en',
timezone VARCHAR(255) NOT NULL DEFAULT 'UTC',
first_day_of_week INTEGER NOT NULL DEFAULT 1,
avatar_image VARCHAR(255),
telegram_bot_token VARCHAR(255),
telegram_chat_id VARCHAR(255),
task_summary_enabled TINYINT(1) NOT NULL DEFAULT 0,
task_summary_frequency VARCHAR(255) DEFAULT 'daily',
task_summary_last_run DATETIME,
task_summary_next_run DATETIME,
telegram_allowed_users TEXT,
task_intelligence_enabled TINYINT(1) NOT NULL DEFAULT 1,
auto_suggest_next_actions_enabled TINYINT(1) NOT NULL DEFAULT 0,
pomodoro_enabled TINYINT(1) NOT NULL DEFAULT 1,
productivity_assistant_enabled TINYINT(1) NOT NULL DEFAULT 1,
next_task_suggestion_enabled TINYINT(1) NOT NULL DEFAULT 1,
today_settings JSON,
sidebar_settings JSON,
ui_settings JSON,
notification_preferences JSON,
keyboard_shortcuts JSON,
email_verified TINYINT(1) NOT NULL DEFAULT 1,
email_verification_token VARCHAR(255),
email_verification_token_expires_at DATETIME,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
ai_provider VARCHAR(255) NOT NULL DEFAULT 'openai',
openai_api_key VARCHAR(255),
ollama_base_url VARCHAR(255) DEFAULT 'http://localhost:11434',
ollama_model VARCHAR(255) DEFAULT 'llama3'
);
`);
const [columns] = await queryInterface.sequelize.query(
'PRAGMA table_info(users);'
);
const hasPasswordDigest = columns.some(
(col) => col.name === 'password_digest'
);
const hasPassword = columns.some((col) => col.name === 'password');
const passwordColumn = hasPasswordDigest
? 'password_digest'
: hasPassword
? 'password'
: null;
if (!passwordColumn) {
throw new Error(
'Neither password nor password_digest column found in users table'
);
}
const existingColumns = new Set(columns.map((col) => col.name));
const baseColumns = [
'id',
'uid',
'name',
'surname',
'email',
'appearance',
'language',
'timezone',
'first_day_of_week',
'avatar_image',
'telegram_bot_token',
'telegram_chat_id',
'task_summary_enabled',
'task_summary_frequency',
'task_summary_last_run',
'task_summary_next_run',
'telegram_allowed_users',
'task_intelligence_enabled',
'auto_suggest_next_actions_enabled',
'pomodoro_enabled',
'productivity_assistant_enabled',
'next_task_suggestion_enabled',
'today_settings',
'sidebar_settings',
'ui_settings',
'notification_preferences',
'keyboard_shortcuts',
'email_verified',
'email_verification_token',
'email_verification_token_expires_at',
'created_at',
'updated_at',
];
const aiColumns = [
'ai_provider',
'openai_api_key',
'ollama_base_url',
'ollama_model',
];
const columnsToSelect = baseColumns.filter((col) =>
existingColumns.has(col)
);
const aiColumnsToSelect = aiColumns.filter((col) =>
existingColumns.has(col)
);
const columnsWithoutPassword = columnsToSelect.filter(
(col) => col !== passwordColumn && col !== 'password_digest'
);
const insertColumns = [
...columnsWithoutPassword,
'password_digest',
...aiColumnsToSelect,
];
const selectColumns = [
...columnsWithoutPassword,
`${passwordColumn} as password_digest`,
...aiColumnsToSelect,
];
const insertColumnsStr = insertColumns.join(', ');
const selectColumnsStr = selectColumns.join(', ');
await queryInterface.sequelize.query(`
INSERT INTO users_new (${insertColumnsStr})
SELECT ${selectColumnsStr}
FROM users;
`);
await queryInterface.sequelize.query('DROP TABLE users;');
await queryInterface.sequelize.query(
'ALTER TABLE users_new RENAME TO users;'
);
await queryInterface.sequelize.query('PRAGMA foreign_keys = ON;');
},
async down(queryInterface, Sequelize) {
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF;');
await queryInterface.sequelize.query('DROP TABLE IF EXISTS users_new;');
await queryInterface.sequelize.query(`
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uid VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255),
surname VARCHAR(255),
email VARCHAR(255) NOT NULL UNIQUE,
password_digest VARCHAR(255) NOT NULL,
appearance VARCHAR(255) NOT NULL DEFAULT 'light',
language VARCHAR(255) NOT NULL DEFAULT 'en',
timezone VARCHAR(255) NOT NULL DEFAULT 'UTC',
first_day_of_week INTEGER NOT NULL DEFAULT 1,
avatar_image VARCHAR(255),
telegram_bot_token VARCHAR(255),
telegram_chat_id VARCHAR(255),
task_summary_enabled TINYINT(1) NOT NULL DEFAULT 0,
task_summary_frequency VARCHAR(255) DEFAULT 'daily',
task_summary_last_run DATETIME,
task_summary_next_run DATETIME,
telegram_allowed_users TEXT,
task_intelligence_enabled TINYINT(1) NOT NULL DEFAULT 1,
auto_suggest_next_actions_enabled TINYINT(1) NOT NULL DEFAULT 0,
pomodoro_enabled TINYINT(1) NOT NULL DEFAULT 1,
productivity_assistant_enabled TINYINT(1) NOT NULL DEFAULT 1,
next_task_suggestion_enabled TINYINT(1) NOT NULL DEFAULT 1,
today_settings JSON,
sidebar_settings JSON,
ui_settings JSON,
notification_preferences JSON,
keyboard_shortcuts JSON,
email_verified TINYINT(1) NOT NULL DEFAULT 1,
email_verification_token VARCHAR(255),
email_verification_token_expires_at DATETIME,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
ai_provider VARCHAR(255) NOT NULL DEFAULT 'openai',
openai_api_key VARCHAR(255),
ollama_base_url VARCHAR(255) DEFAULT 'http://localhost:11434',
ollama_model VARCHAR(255) DEFAULT 'llama3'
);
`);
const [columns] = await queryInterface.sequelize.query(
'PRAGMA table_info(users);'
);
const existingColumns = new Set(columns.map((col) => col.name));
const baseColumns = [
'id',
'uid',
'name',
'surname',
'email',
'password_digest',
'appearance',
'language',
'timezone',
'first_day_of_week',
'avatar_image',
'telegram_bot_token',
'telegram_chat_id',
'task_summary_enabled',
'task_summary_frequency',
'task_summary_last_run',
'task_summary_next_run',
'telegram_allowed_users',
'task_intelligence_enabled',
'auto_suggest_next_actions_enabled',
'pomodoro_enabled',
'productivity_assistant_enabled',
'next_task_suggestion_enabled',
'today_settings',
'sidebar_settings',
'ui_settings',
'notification_preferences',
'keyboard_shortcuts',
'email_verified',
'email_verification_token',
'email_verification_token_expires_at',
'created_at',
'updated_at',
];
const aiColumns = [
'ai_provider',
'openai_api_key',
'ollama_base_url',
'ollama_model',
];
const columnsToSelect = [
...baseColumns.filter((col) => existingColumns.has(col)),
...aiColumns.filter((col) => existingColumns.has(col)),
];
const insertColumnsStr = columnsToSelect.join(', ');
const selectColumnsStr = columnsToSelect.join(', ');
await queryInterface.sequelize.query(`
INSERT INTO users_new (${insertColumnsStr})
SELECT ${selectColumnsStr}
FROM users;
`);
await queryInterface.sequelize.query('DROP TABLE users;');
await queryInterface.sequelize.query(
'ALTER TABLE users_new RENAME TO users;'
);
await queryInterface.sequelize.query('PRAGMA foreign_keys = ON;');
},
};