feat: implement identity and security features

- Added account types and privileged account types constants.
- Created admin user controller for updating user security fields.
- Developed role assignment controller for managing user roles.
- Implemented validation middleware for request schemas.
- Defined user role and auth session models for database interactions.
- Created services for authentication, email notifications, and OTP handling.
- Developed OAuth service for Google and Apple authentication.
- Added JWT utility functions for token generation and verification.
- Implemented comprehensive tests for authentication, session management, and password policies.
- Created migration for updating user schema and adding new tables for auth sessions and user identities.
This commit is contained in:
Sathira Sri Sathara
2026-09-03 13:56:18 +05:30
parent 267e80e2ec
commit 9d3d431416
54 changed files with 1389 additions and 1076 deletions
@@ -0,0 +1,61 @@
"use strict";
module.exports = {
async up(queryInterface, Sequelize) {
const userColumns = await queryInterface.describeTable("users");
await queryInterface.changeColumn("users", "accountType", {
type: Sequelize.ENUM("superadmin", "admin", "manager", "business_customer", "rider", "customer", "support_agent"),
allowNull: false, defaultValue: "customer",
});
await queryInterface.changeColumn("users", "password", { type: Sequelize.STRING, allowNull: true });
if (!userColumns.passwordChangedAt) await queryInterface.addColumn("users", "passwordChangedAt", { type: Sequelize.DATE, allowNull: true });
await queryInterface.addColumn("users", "tokenVersion", { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 });
await queryInterface.addColumn("users", "lastLoginAt", { type: Sequelize.DATE, allowNull: true });
await queryInterface.createTable("auth_sessions", {
id: { type: Sequelize.UUID, primaryKey: true }, user_id: { type: Sequelize.STRING, allowNull: false, references: { model: "users", key: "id" }, onDelete: "CASCADE" },
token_family_id: { type: Sequelize.UUID, allowNull: false }, refresh_token_hash: { type: Sequelize.STRING(64), allowNull: false },
device_name: Sequelize.STRING, user_agent: Sequelize.STRING(500), ip_address: Sequelize.STRING(64),
remember_me: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false }, token_version: { type: Sequelize.INTEGER, allowNull: false },
last_used_at: Sequelize.DATE, expires_at: { type: Sequelize.DATE, allowNull: false }, revoked_at: Sequelize.DATE,
revoked_reason: Sequelize.STRING, replaced_by_session_id: Sequelize.UUID,
createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex("auth_sessions", ["user_id"]);
await queryInterface.addIndex("auth_sessions", ["token_family_id"]);
await queryInterface.addIndex("auth_sessions", ["expires_at"]);
await queryInterface.createTable("user_identities", {
id: { type: Sequelize.UUID, primaryKey: true }, user_id: { type: Sequelize.STRING, allowNull: false, references: { model: "users", key: "id" }, onDelete: "CASCADE" },
provider: { type: Sequelize.ENUM("google", "apple"), allowNull: false }, provider_subject: { type: Sequelize.STRING, allowNull: false },
provider_email: Sequelize.STRING, createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex("user_identities", ["provider", "provider_subject"], { unique: true, name: "user_identity_provider_subject_unique" });
await queryInterface.addIndex("user_identities", ["user_id"]);
await queryInterface.createTable("user_roles", {
id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
user_id: { type: Sequelize.STRING, allowNull: false, references: { model: "users", key: "id" }, onDelete: "CASCADE" },
role_id: { type: Sequelize.STRING, allowNull: false, references: { model: "roles", key: "role_id" }, onDelete: "CASCADE" },
createdAt: { type: Sequelize.DATE, allowNull: false }, updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addIndex("user_roles", ["user_id", "role_id"], { unique: true, name: "user_role_unique" });
await queryInterface.addIndex("rolePermission", ["role_id", "permission_id"], { unique: true, name: "role_permission_unique" });
await queryInterface.addIndex("userPermission", ["user_id", "permission_id"], { unique: true, name: "user_permission_unique" });
await queryInterface.addIndex("roles", ["roleName"], { unique: true, name: "role_name_unique" });
},
async down(queryInterface, Sequelize) {
await queryInterface.removeIndex("roles", "role_name_unique");
await queryInterface.removeIndex("userPermission", "user_permission_unique");
await queryInterface.removeIndex("rolePermission", "role_permission_unique");
await queryInterface.dropTable("user_roles");
await queryInterface.dropTable("user_identities");
await queryInterface.dropTable("auth_sessions");
await queryInterface.removeColumn("users", "lastLoginAt");
await queryInterface.removeColumn("users", "tokenVersion");
// passwordChangedAt may predate this migration, so down intentionally preserves it.
await queryInterface.changeColumn("users", "password", { type: Sequelize.STRING, allowNull: false });
await queryInterface.changeColumn("users", "accountType", { type: Sequelize.ENUM("business_customer", "customer"), defaultValue: "customer" });
},
};