This commit is contained in:
jackiettran
2026-01-16 18:04:39 -05:00
parent 63385e049c
commit cf97dffbfb
31 changed files with 4405 additions and 56 deletions

View File

@@ -0,0 +1,107 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
// Add TOTP configuration fields
await queryInterface.addColumn("Users", "twoFactorEnabled", {
type: Sequelize.BOOLEAN,
defaultValue: false,
allowNull: false,
});
await queryInterface.addColumn("Users", "twoFactorMethod", {
type: Sequelize.ENUM("totp", "email"),
allowNull: true,
});
await queryInterface.addColumn("Users", "totpSecret", {
type: Sequelize.STRING,
allowNull: true,
});
await queryInterface.addColumn("Users", "totpSecretIv", {
type: Sequelize.STRING,
allowNull: true,
});
await queryInterface.addColumn("Users", "twoFactorEnabledAt", {
type: Sequelize.DATE,
allowNull: true,
});
// Add Email OTP fields (backup method)
await queryInterface.addColumn("Users", "emailOtpCode", {
type: Sequelize.STRING,
allowNull: true,
});
await queryInterface.addColumn("Users", "emailOtpExpiry", {
type: Sequelize.DATE,
allowNull: true,
});
await queryInterface.addColumn("Users", "emailOtpAttempts", {
type: Sequelize.INTEGER,
defaultValue: 0,
allowNull: false,
});
// Add Recovery Codes fields
await queryInterface.addColumn("Users", "recoveryCodesHash", {
type: Sequelize.TEXT,
allowNull: true,
});
await queryInterface.addColumn("Users", "recoveryCodesGeneratedAt", {
type: Sequelize.DATE,
allowNull: true,
});
await queryInterface.addColumn("Users", "recoveryCodesUsedCount", {
type: Sequelize.INTEGER,
defaultValue: 0,
allowNull: false,
});
// Add Step-up session tracking
await queryInterface.addColumn("Users", "twoFactorVerifiedAt", {
type: Sequelize.DATE,
allowNull: true,
});
// Add temporary secret storage during setup
await queryInterface.addColumn("Users", "twoFactorSetupPendingSecret", {
type: Sequelize.STRING,
allowNull: true,
});
await queryInterface.addColumn("Users", "twoFactorSetupPendingSecretIv", {
type: Sequelize.STRING,
allowNull: true,
});
},
async down(queryInterface, Sequelize) {
// Remove all 2FA fields in reverse order
await queryInterface.removeColumn("Users", "twoFactorSetupPendingSecretIv");
await queryInterface.removeColumn("Users", "twoFactorSetupPendingSecret");
await queryInterface.removeColumn("Users", "twoFactorVerifiedAt");
await queryInterface.removeColumn("Users", "recoveryCodesUsedCount");
await queryInterface.removeColumn("Users", "recoveryCodesGeneratedAt");
await queryInterface.removeColumn("Users", "recoveryCodesHash");
await queryInterface.removeColumn("Users", "emailOtpAttempts");
await queryInterface.removeColumn("Users", "emailOtpExpiry");
await queryInterface.removeColumn("Users", "emailOtpCode");
await queryInterface.removeColumn("Users", "twoFactorEnabledAt");
await queryInterface.removeColumn("Users", "totpSecretIv");
await queryInterface.removeColumn("Users", "totpSecret");
await queryInterface.removeColumn("Users", "twoFactorMethod");
await queryInterface.removeColumn("Users", "twoFactorEnabled");
// Remove the ENUM type
await queryInterface.sequelize.query(
'DROP TYPE IF EXISTS "enum_Users_twoFactorMethod";'
);
},
};

View File

@@ -0,0 +1,32 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
// Add recentTotpCodes field for TOTP replay protection
await queryInterface.addColumn("Users", "recentTotpCodes", {
type: Sequelize.TEXT,
allowNull: true,
comment: "JSON array of hashed recently used TOTP codes for replay protection",
});
// Remove deprecated columns (if they exist)
await queryInterface.removeColumn("Users", "twoFactorEnabledAt").catch(() => {});
await queryInterface.removeColumn("Users", "recoveryCodesUsedCount").catch(() => {});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn("Users", "recentTotpCodes");
// Re-add deprecated columns for rollback
await queryInterface.addColumn("Users", "twoFactorEnabledAt", {
type: Sequelize.DATE,
allowNull: true,
});
await queryInterface.addColumn("Users", "recoveryCodesUsedCount", {
type: Sequelize.INTEGER,
defaultValue: 0,
allowNull: false,
});
},
};