mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: achievement_definitions.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Admin-managed catalog of achievements (badges and milestones).
|
||||
* Replaces the old static ACHIEVEMENT_REGISTRY in data/achievements.data.js.
|
||||
* `is_system` rows are the built-in keys referenced by name in
|
||||
* services/achievements.service.js's trigger functions — protected
|
||||
* from deletion/key-rename by the admin controller.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 3, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
|
||||
const mdl_AchievementDefinitions = sequelize.define('AchievementDefinition', {
|
||||
achievement_definition_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'ID', hidden: true, order: 0 },
|
||||
key: { type: DataTypes.STRING(100), allowNull: false, unique: true, label: 'Key', hidden: false, order: 1, filterable: true },
|
||||
type: { type: DataTypes.ENUM('badge', 'milestone'), allowNull: false, defaultValue: 'badge', label: 'Type', hidden: false, order: 2, filterable: true },
|
||||
label: { type: DataTypes.STRING(255), allowNull: false, label: 'Label', hidden: false, order: 3, filterable: true },
|
||||
description: { type: DataTypes.TEXT, allowNull: true, label: 'Description', hidden: false, order: 4, filterable: false },
|
||||
icon: { type: DataTypes.STRING(50), allowNull: true, label: 'Icon', hidden: false, order: 5, filterable: false },
|
||||
trigger: { type: DataTypes.STRING(30), allowNull: true, label: 'Trigger', hidden: false, order: 6, filterable: true },
|
||||
is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, label: 'Active', hidden: false, order: 7, filterable: true },
|
||||
is_system: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, label: 'System', hidden: false, order: 8, filterable: true },
|
||||
}, {
|
||||
tableName: 'achievement_definitions',
|
||||
timestamps: true,
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
module.exports = mdl_AchievementDefinitions;
|
||||
@@ -0,0 +1,39 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: achievements.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: User achievements — badges and milestones.
|
||||
* Unique per user + key (one achievement of each type per user).
|
||||
*
|
||||
* Built-in keys:
|
||||
* Badge:
|
||||
* early_access — registered before Dec 31, 2026
|
||||
* premium_first_time — first premium tier purchase
|
||||
* exclusive_first_time — first exclusive tier purchase
|
||||
* Milestone:
|
||||
* course_completed — completed first course
|
||||
* (extend as needed)
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 11, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
|
||||
const mdl_Achievements = sequelize.define('Achievement', {
|
||||
achievement_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'Achievement ID', hidden: true, order: 0 },
|
||||
user_id: { type: DataTypes.BIGINT, allowNull: false, label: 'User ID', hidden: true, order: 0 },
|
||||
type: { type: DataTypes.ENUM('badge', 'milestone'), allowNull: false, defaultValue: 'badge', label: 'Type', hidden: false, order: 1, filterable: true },
|
||||
key: { type: DataTypes.STRING(100), allowNull: false, label: 'Key', hidden: false, order: 2, filterable: true },
|
||||
label: { type: DataTypes.STRING(255), allowNull: false, label: 'Label', hidden: false, order: 3, filterable: true },
|
||||
description: { type: DataTypes.TEXT, allowNull: true, label: 'Description', hidden: false, order: 4, filterable: false },
|
||||
icon: { type: DataTypes.STRING(50), allowNull: true, label: 'Icon', hidden: false, order: 5, filterable: false },
|
||||
granted_by: { type: DataTypes.BIGINT, allowNull: true, label: 'Granted By', hidden: false, order: 5, filterable: false },
|
||||
granted_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, label: 'Granted At', hidden: false, order: 6, filterable: false },
|
||||
metadata: { type: DataTypes.JSONB, allowNull: true, defaultValue: {}, label: 'Metadata', hidden: true, order: 0 },
|
||||
}, {
|
||||
tableName: 'achievements',
|
||||
timestamps: true,
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
module.exports = mdl_Achievements;
|
||||
@@ -0,0 +1,58 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: trusted_devices.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Sequelize model for the `trusted_devices` table.
|
||||
* One rolling row per (user_id, fingerprint_hash) — lets a login
|
||||
* from an already-verified device skip the OTP gate until the
|
||||
* trust window lapses or is explicitly revoked.
|
||||
* Has a Many-to-One relationship with Users and UserSessions.
|
||||
* Author: Kenneth Obsequio
|
||||
* Date Created: Jul. 5, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_Users = require('./users.mdl');
|
||||
const mdl_UserSessions = require('./user_sessions.mdl');
|
||||
|
||||
const mdl_TrustedDevices = sequelize.define('TrustedDevices', {
|
||||
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
references: { model: mdl_Users, key: 'user_id' },
|
||||
},
|
||||
|
||||
// SHA-256 of the opaque token stored in the `device_trust` cookie.
|
||||
device_token_hash: { type: DataTypes.TEXT, allowNull: false },
|
||||
|
||||
// SHA-256 of `browser|os|device` parsed from the User-Agent header.
|
||||
fingerprint_hash: { type: DataTypes.TEXT, allowNull: false },
|
||||
|
||||
// Most recent user_sessions row minted for this device — lets a single
|
||||
// session termination revoke just this device's trust.
|
||||
last_session_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
references: { model: mdl_UserSessions, key: 'session_id' },
|
||||
},
|
||||
|
||||
expires_at: { type: DataTypes.DATE, allowNull: false },
|
||||
revoked_at: { type: DataTypes.DATE, allowNull: true },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Modified By" },
|
||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
|
||||
}, {
|
||||
tableName: 'trusted_devices',
|
||||
timestamps: true,
|
||||
paranoid: true, // enables soft delete — sets deleted_at instead of DELETE
|
||||
});
|
||||
|
||||
// Associations
|
||||
mdl_TrustedDevices.belongsTo(mdl_Users, { foreignKey: 'user_id' });
|
||||
mdl_Users.hasMany(mdl_TrustedDevices, { foreignKey: 'user_id' });
|
||||
|
||||
mdl_TrustedDevices.belongsTo(mdl_UserSessions, { foreignKey: 'last_session_id' });
|
||||
|
||||
module.exports = mdl_TrustedDevices;
|
||||
@@ -0,0 +1,30 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_Users = require('./users.mdl');
|
||||
|
||||
const mdl_UserActivity = sequelize.define('UserActivity', {
|
||||
activity_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
user_id: { type: DataTypes.BIGINT, allowNull: false, references: { model: mdl_Users, key: 'user_id' } },
|
||||
session_id: { type: DataTypes.BIGINT, allowNull: true },
|
||||
action: { type: DataTypes.STRING(100), allowNull: false },
|
||||
entity_type: { type: DataTypes.STRING(50), allowNull: true },
|
||||
entity_id: { type: DataTypes.BIGINT, allowNull: true },
|
||||
/**
|
||||
* details JSONB — free-form context per action, e.g.:
|
||||
* login : { session_id, reg_type }
|
||||
* deactivate_user : { target_email }
|
||||
* lesson_read : { lesson_uuid, status }
|
||||
* submit_task : { task_id }
|
||||
* set_user_status : { is_active }
|
||||
*/
|
||||
details: { type: DataTypes.JSONB, allowNull: true },
|
||||
created_at: { type: DataTypes.DATE, allowNull: true, defaultValue: DataTypes.NOW },
|
||||
}, {
|
||||
tableName: 'user_activity',
|
||||
timestamps: false,
|
||||
});
|
||||
|
||||
mdl_UserActivity.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
|
||||
mdl_Users.hasMany(mdl_UserActivity, { foreignKey: 'user_id', as: 'activities' });
|
||||
|
||||
module.exports = mdl_UserActivity;
|
||||
@@ -0,0 +1,41 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: user_bans.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Sequelize model for the `user_bans` table.
|
||||
* Stores ban/unban audit records for policy enforcement actions.
|
||||
* Distinct from deactivation (account lifecycle) — bans track WHY,
|
||||
* WHO banned, duration, and lift history.
|
||||
* Author: Kenneth Obsequio
|
||||
* Date Created: Jun. 27, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_Users = require('./users.mdl');
|
||||
|
||||
const mdl_UserBans = sequelize.define('UserBan', {
|
||||
ban_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
user_id: { type: DataTypes.BIGINT, allowNull: false, references: { model: mdl_Users, key: 'user_id' } },
|
||||
banned_by: { type: DataTypes.BIGINT, allowNull: false, references: { model: mdl_Users, key: 'user_id' } },
|
||||
|
||||
reason: { type: DataTypes.TEXT, allowNull: false },
|
||||
ban_type: { type: DataTypes.ENUM('temporary', 'permanent'), allowNull: false },
|
||||
|
||||
banned_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
|
||||
expires_at: { type: DataTypes.DATE, allowNull: true },
|
||||
|
||||
is_lifted: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
|
||||
lifted_at: { type: DataTypes.DATE, allowNull: true },
|
||||
lifted_by: { type: DataTypes.BIGINT, allowNull: true, references: { model: mdl_Users, key: 'user_id' } },
|
||||
lift_reason: { type: DataTypes.TEXT, allowNull: true },
|
||||
}, {
|
||||
tableName: 'user_bans',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
// ─── Associations ──────────────────────────────────────────────────────────────
|
||||
mdl_UserBans.belongsTo(mdl_Users, { as: 'user', foreignKey: 'user_id' });
|
||||
mdl_UserBans.belongsTo(mdl_Users, { as: 'banner', foreignKey: 'banned_by' });
|
||||
mdl_UserBans.belongsTo(mdl_Users, { as: 'lifter', foreignKey: 'lifted_by' });
|
||||
mdl_Users.hasMany(mdl_UserBans, { as: 'bans', foreignKey: 'user_id' });
|
||||
|
||||
module.exports = mdl_UserBans;
|
||||
@@ -0,0 +1,38 @@
|
||||
const excludeAttributes = [
|
||||
];
|
||||
|
||||
const jsonbSchemas = {
|
||||
// Add here
|
||||
};
|
||||
|
||||
// Different exclude sets per role
|
||||
const adminExclude = [
|
||||
...excludeAttributes,
|
||||
// admins can see audit fields, so nothing extra excluded
|
||||
];
|
||||
|
||||
const userExclude = [
|
||||
...excludeAttributes,
|
||||
// regular users cannot see audit trails
|
||||
"created_by", "updated_by", "deleted_by",
|
||||
"deleted_at",
|
||||
];
|
||||
|
||||
const computedAttributes = [
|
||||
{
|
||||
key: "memberCount",
|
||||
label: "Members",
|
||||
type: "number",
|
||||
order: 5,
|
||||
literal: `(
|
||||
SELECT CAST(COUNT(*) AS INTEGER)
|
||||
FROM "user_group_members"
|
||||
JOIN "users" ON "users"."user_id" = "user_group_members"."user_id"
|
||||
WHERE "user_group_members"."group_id" = "UserGroup"."group_id"
|
||||
AND "user_group_members"."deletedAt" IS NULL
|
||||
AND "users"."deletedAt" IS NULL
|
||||
)`,
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = { excludeAttributes, adminExclude, userExclude, jsonbSchemas, computedAttributes };
|
||||
@@ -0,0 +1,86 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: user_groups.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Sequelize models for `user_groups` and `user_group_members` tables.
|
||||
* Implements a Many-to-Many self-contained group system.
|
||||
* Groups are used as named permission bundles.
|
||||
* Author: rgrgogu
|
||||
* Date Created: Oct. 6, 2025
|
||||
***********************************************************************************************************************************************************************
|
||||
* Change History:
|
||||
* DATE AUTHOR LOG DESCRIPTION
|
||||
* Oct. 6, 2025 rgrgogu 001 Initial creation - STAR Phase 1
|
||||
* May 23, 2026 rgrgogu 002 Added group_code — unique invite code for self-registration
|
||||
***********************************************************************************************************************************************************************
|
||||
* HOW TO USE:
|
||||
* const { mdl_UserGroups, mdl_UserGroupMembers } = require('./models/user_groups.mdl');
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_Users = require('./users.mdl');
|
||||
|
||||
// ─── UserGroups ────────────────────────────────────────────────────────────────
|
||||
const mdl_UserGroups = sequelize.define('UserGroup', {
|
||||
group_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "Group ID", order: 1, hidden: true },
|
||||
name: { type: DataTypes.STRING(50), allowNull: false, label: "Group Name", order: 2 },
|
||||
/**
|
||||
* group_code — unique, human-readable invite code distributed to users.
|
||||
* Used in the self-registration URL: /register?group_code=<value>
|
||||
* Automatically uppercased via a beforeValidate hook below.
|
||||
* Example: "SALES-2025", "ONBOARD-Q1"
|
||||
*/
|
||||
group_code: { type: DataTypes.STRING(50), allowNull: true, unique: true, label: "Group Code", order: 3 },
|
||||
description:{ type: DataTypes.TEXT, label: "Description", order: 4, hidden: true },
|
||||
is_active: { type: DataTypes.BOOLEAN, defaultValue: true, label: "Active", order: 5 },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Modified By" },
|
||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
|
||||
}, {
|
||||
tableName: 'user_groups',
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
hooks: {
|
||||
// Always store group_code in uppercase to make lookups case-insensitive
|
||||
beforeValidate: (group) => {
|
||||
if (group.group_code) group.group_code = group.group_code.toUpperCase().trim();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Junction: UserGroupMembers ────────────────────────────────────────────────
|
||||
const mdl_UserGroupMembers = sequelize.define('UserGroupMember', {
|
||||
group_id: { type: DataTypes.BIGINT, primaryKey: true },
|
||||
user_id: { type: DataTypes.BIGINT, primaryKey: true },
|
||||
joined_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Modified By" },
|
||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
|
||||
}, {
|
||||
tableName: 'user_group_members',
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
});
|
||||
|
||||
// ─── Associations ──────────────────────────────────────────────────────────────
|
||||
mdl_Users.belongsToMany(mdl_UserGroups, { through: mdl_UserGroupMembers, foreignKey: 'user_id', otherKey: 'group_id', as: 'groups' });
|
||||
mdl_UserGroups.belongsToMany(mdl_Users, { through: mdl_UserGroupMembers, foreignKey: 'group_id', otherKey: 'user_id', as: 'members' });
|
||||
|
||||
mdl_UserGroups.hasMany(mdl_UserGroupMembers, { foreignKey: 'group_id' });
|
||||
mdl_UserGroupMembers.belongsTo(mdl_UserGroups, { foreignKey: 'group_id' });
|
||||
|
||||
mdl_Users.hasMany(mdl_UserGroupMembers, { foreignKey: 'user_id' });
|
||||
mdl_UserGroupMembers.belongsTo(mdl_Users, { foreignKey: 'user_id' });
|
||||
|
||||
mdl_UserGroups.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
|
||||
mdl_UserGroups.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' });
|
||||
mdl_UserGroups.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' });
|
||||
|
||||
mdl_Users.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
|
||||
mdl_Users.belongsTo(mdl_Users, { as: 'modifier', foreignKey: 'updatedBy' });
|
||||
mdl_Users.belongsTo(mdl_Users, { as: 'deleter', foreignKey: 'deletedBy' });
|
||||
|
||||
module.exports = { mdl_UserGroups, mdl_UserGroupMembers };
|
||||
@@ -0,0 +1,60 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: user_sessions.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Sequelize model for the `user_sessions` table.
|
||||
* Captures login and logout audit data (IP, geo, device) as JSONB.
|
||||
* Has a Many-to-One relationship with Users.
|
||||
* Author: rgrgogu
|
||||
* Date Created: Oct. 6, 2025
|
||||
***********************************************************************************************************************************************************************
|
||||
* Change History:
|
||||
* DATE AUTHOR LOG DESCRIPTION
|
||||
* Oct. 6, 2025 rgrgogu 001 Initial creation - STAR Phase 1
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_Users = require('./users.mdl');
|
||||
|
||||
const mdl_UserSessions = sequelize.define('UserSessions', {
|
||||
session_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
references: { model: mdl_Users, key: 'user_id' },
|
||||
},
|
||||
/**
|
||||
* login_info / logout_info JSONB:
|
||||
* {
|
||||
* date: ISO string,
|
||||
* ip_address: string,
|
||||
* country: string,
|
||||
* region: string,
|
||||
* city: string,
|
||||
* lat: number,
|
||||
* long: number,
|
||||
* device_info: { ua, browser, os, device }
|
||||
* forced_by: user_id
|
||||
* }
|
||||
*/
|
||||
login_info: { type: DataTypes.JSONB, allowNull: true },
|
||||
logout_info: { type: DataTypes.JSONB, allowNull: true },
|
||||
|
||||
// Store the refresh-token hash so we can invalidate individual sessions
|
||||
refresh_token_hash: { type: DataTypes.TEXT, allowNull: true },
|
||||
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Modified By" },
|
||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
|
||||
}, {
|
||||
tableName: 'user_sessions',
|
||||
timestamps: true,
|
||||
paranoid: true, // enables soft delete — sets deleted_at instead of DELETE
|
||||
});
|
||||
|
||||
// Associations
|
||||
mdl_UserSessions.belongsTo(mdl_Users, { foreignKey: 'user_id' });
|
||||
mdl_Users.hasMany(mdl_UserSessions, { foreignKey: 'user_id' });
|
||||
|
||||
module.exports = mdl_UserSessions;
|
||||
@@ -0,0 +1,59 @@
|
||||
const excludeAttributes = [
|
||||
"password", "otp_code", "otp_expires_at", 'must_change_password', 'password_expires_at', "needs_intro",
|
||||
"personal_info.name.given_name",
|
||||
"personal_info.name.middle_name",
|
||||
"personal_info.name.last_name",
|
||||
"personal_info.name.extension_name",
|
||||
"personal_info.addresses[]",
|
||||
"personal_info.addresses[].city",
|
||||
"personal_info.addresses[].country",
|
||||
"personal_info.addresses[].street",
|
||||
"personal_info.addresses[].zip",
|
||||
"personal_info.addresses[].address_type",
|
||||
"personal_info.addresses[].state",
|
||||
"personal_info.phone_number[]",
|
||||
"personal_info.phone_number[].country_code",
|
||||
"personal_info.phone_number[].number",
|
||||
"personal_info.phone_number[].phone_type",
|
||||
];
|
||||
|
||||
const jsonbSchemas = {
|
||||
personal_info: {
|
||||
name: {
|
||||
full_name: { type: "text", label: "Full Name", order: 2 }, // ← slot 2
|
||||
},
|
||||
date_of_birth: { type: "date", label: "Date of Birth", order: 4 },
|
||||
occupation: { type: "text", label: "Occupation", order: 5 },
|
||||
// addresses: {
|
||||
// full_address: { type: "text", label: "Full Address" },
|
||||
// },
|
||||
// phone_number: {
|
||||
// full_number: { type: "text", label: "Phone Number" },
|
||||
// },
|
||||
},
|
||||
};
|
||||
|
||||
// Different exclude sets per role
|
||||
const adminExclude = [
|
||||
...excludeAttributes,
|
||||
// admins can see audit fields, so nothing extra excluded
|
||||
];
|
||||
|
||||
const userExclude = [
|
||||
...excludeAttributes,
|
||||
// regular users cannot see audit trails
|
||||
"created_by", "updated_by", "deleted_by",
|
||||
"deleted_at",
|
||||
];
|
||||
|
||||
const computedAttributes = [
|
||||
{
|
||||
key: "groups",
|
||||
label: "Groups",
|
||||
type: "array", // tells the paginator this is a pre-joined association array
|
||||
order: 5,
|
||||
filterable: true, // handled specially in the controller — see extractGroupsFilter()
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = { excludeAttributes, adminExclude, userExclude, jsonbSchemas, computedAttributes };
|
||||
@@ -0,0 +1,68 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: users.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Sequelize model for the `users` table.
|
||||
* Stores authentication credentials, account flags, registration type,
|
||||
* role/account type, and a flexible JSONB personal_info column.
|
||||
* Author: rgrgogu
|
||||
* Date Created: Oct. 6, 2025
|
||||
***********************************************************************************************************************************************************************
|
||||
* Change History:
|
||||
* DATE AUTHOR LOG DESCRIPTION
|
||||
* Oct. 6, 2025 rgrgogu 001 Initial creation - STAR Phase 1
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
|
||||
const mdl_Users = sequelize.define('User', {
|
||||
user_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "User ID", order: 1, hidden: true },
|
||||
email: { type: DataTypes.STRING(255), allowNull: false, unique: true, label: "Email Address", validate: { isEmail: true }, order: 3 },
|
||||
password: { type: DataTypes.TEXT, label: "Password" },
|
||||
is_active: { type: DataTypes.BOOLEAN, defaultValue: true, label: "Active", order: 6 },
|
||||
is_verified: { type: DataTypes.BOOLEAN, defaultValue: false, label: "Verified", order: 7 },
|
||||
reg_type: { type: DataTypes.ENUM('google', 'system'), defaultValue: 'system', label: "Registration Type", order: 8 },
|
||||
/**
|
||||
* acc_type drives RBAC:
|
||||
* - "user" → Client endpoints only
|
||||
* - "staff" → Client + Staff endpoints
|
||||
* - "admin" → All endpoints
|
||||
*/
|
||||
acc_type: { type: DataTypes.ENUM('admin', 'staff', 'user'), defaultValue: 'user', label: "Account Type", order: 9 },
|
||||
needs_intro: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, label: "Needs Intro" },
|
||||
/**
|
||||
* personal_info JSONB structure:
|
||||
* {
|
||||
* name: { given_name, middle_name, last_name, extension_name, full_name },
|
||||
* occupation: string,
|
||||
* addresses: [{ street, city, state, zip, country, address_type, full_address }],
|
||||
* phone_number: [{ number, country_code, phone_type, full_number }],
|
||||
* date_of_birth: date,
|
||||
* avatar: { mime_type, name, size, url, uuid },
|
||||
* album: [{ uuid, file_url, original_name, uploaded_at, order_index }]
|
||||
* }
|
||||
*/
|
||||
personal_info: { type: DataTypes.JSONB, defaultValue: null, label: "Personal Info", order: 2 },
|
||||
|
||||
// ── Ban state ────────────────────────────────────────────────────────────────
|
||||
is_banned: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false, label: "Banned" },
|
||||
ban_expires_at: { type: DataTypes.DATE, defaultValue: null, allowNull: true, label: "Ban Expires At" },
|
||||
|
||||
// ── Password policy ─────────────────────────────────────────────────────────
|
||||
must_change_password: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false, label: "Must Change Password" },
|
||||
password_expires_at: { type: DataTypes.DATE, defaultValue: null, allowNull: true, label: "Password Expires At" },
|
||||
|
||||
// OTP fields (stored temporarily during verification flow)
|
||||
otp_code: { type: DataTypes.STRING(6), allowNull: true, label: "OTP Code" },
|
||||
otp_expires_at: { type: DataTypes.DATE, allowNull: true, label: "OTP Expires At" },
|
||||
|
||||
// ── Audit trails ────────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Modified By" },
|
||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
|
||||
}, {
|
||||
tableName: 'users',
|
||||
timestamps: true,
|
||||
paranoid: true, // enables soft delete — sets deleted_at instead of DELETE
|
||||
});
|
||||
|
||||
module.exports = mdl_Users;
|
||||
Reference in New Issue
Block a user