mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,38 +0,0 @@
|
|||||||
require('dotenv').config();
|
|
||||||
const { Sequelize } = require('sequelize');
|
|
||||||
const mdl_Users = require('./models/users/users.mdl');
|
|
||||||
const sequelize = require('./config/db.config');
|
|
||||||
|
|
||||||
const qg = sequelize.getQueryInterface().queryGenerator;
|
|
||||||
|
|
||||||
const order = [[Sequelize.json('personal_info.name.full_name'), 'ASC']];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sql = qg.selectQuery(mdl_Users.getTableName(), {
|
|
||||||
model: mdl_Users,
|
|
||||||
attributes: ['user_id'],
|
|
||||||
order,
|
|
||||||
limit: 10,
|
|
||||||
offset: 0,
|
|
||||||
}, mdl_Users);
|
|
||||||
console.log('SORT SQL:\n', sql);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('SORT ERROR:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter test
|
|
||||||
const { Op } = require('sequelize');
|
|
||||||
const whereCond = Sequelize.where(Sequelize.json('personal_info.name.full_name'), { [Op.iLike]: '%a%' });
|
|
||||||
try {
|
|
||||||
const sql2 = qg.selectQuery(mdl_Users.getTableName(), {
|
|
||||||
model: mdl_Users,
|
|
||||||
attributes: ['user_id'],
|
|
||||||
where: whereCond,
|
|
||||||
limit: 10,
|
|
||||||
offset: 0,
|
|
||||||
}, mdl_Users);
|
|
||||||
console.log('FILTER SQL:\n', sql2);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('FILTER ERROR:', e.message);
|
|
||||||
}
|
|
||||||
process.exit(0);
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
const { Sequelize } = require('sequelize');
|
|
||||||
const mdl_Users = require('./models/users/users.mdl');
|
|
||||||
const { mdl_UserGroups, mdl_UserGroupMembers } = require('./models/users/user_groups.mdl');
|
|
||||||
const sequelize = require('./config/db.config');
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await sequelize.authenticate();
|
|
||||||
console.log('DB connected OK');
|
|
||||||
|
|
||||||
// Find any group_id to test with
|
|
||||||
const anyGroup = await mdl_UserGroups.findOne({ attributes: ['group_id'], paranoid: false });
|
|
||||||
console.log('sample group_id:', anyGroup?.group_id);
|
|
||||||
const group_id = anyGroup?.group_id;
|
|
||||||
|
|
||||||
const order = [[Sequelize.json('personal_info.name.full_name'), 'ASC']];
|
|
||||||
|
|
||||||
const result = await mdl_Users.findAndCountAll({
|
|
||||||
attributes: ['user_id'],
|
|
||||||
order,
|
|
||||||
limit: 3,
|
|
||||||
offset: 0,
|
|
||||||
include: [{
|
|
||||||
model: mdl_UserGroupMembers,
|
|
||||||
where: { group_id },
|
|
||||||
attributes: [],
|
|
||||||
required: true,
|
|
||||||
}],
|
|
||||||
logging: (sql) => console.log('\n[SQL]', sql),
|
|
||||||
});
|
|
||||||
console.log('SORT+JOIN RESULT COUNT:', result.count, 'rows:', result.rows.length);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('ERROR:', e.message);
|
|
||||||
} finally {
|
|
||||||
await sequelize.close();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -337,7 +337,10 @@ exports.getUsersNotInGroup = async (req, res) => {
|
|||||||
const memberIds = members.map((m) => m.user_id);
|
const memberIds = members.map((m) => m.user_id);
|
||||||
|
|
||||||
const users = await mdl_Users.findAll({
|
const users = await mdl_Users.findAll({
|
||||||
where: { user_id: { [Op.notIn]: memberIds.length ? memberIds : [0] } },
|
where: {
|
||||||
|
user_id: { [Op.notIn]: memberIds.length ? memberIds : [0] },
|
||||||
|
acc_type: 'user', // exclude staff/admin — only regular users can be added to a group
|
||||||
|
},
|
||||||
attributes: [
|
attributes: [
|
||||||
'user_id',
|
'user_id',
|
||||||
[Sequelize.literal(`("User"."personal_info"->'name'->>'full_name')`), 'full_name'],
|
[Sequelize.literal(`("User"."personal_info"->'name'->>'full_name')`), 'full_name'],
|
||||||
|
|||||||
@@ -186,6 +186,12 @@ exports.deactivateUser = async (req, res) => {
|
|||||||
await user.update({ is_active: false, deletedBy: req.user.user_id });
|
await user.update({ is_active: false, deletedBy: req.user.user_id });
|
||||||
await user.destroy();
|
await user.destroy();
|
||||||
|
|
||||||
|
await mdl_UserGroupMembers.update(
|
||||||
|
{ deletedBy: req.user.user_id },
|
||||||
|
{ where: { user_id: req.params.id } }
|
||||||
|
);
|
||||||
|
await mdl_UserGroupMembers.destroy({ where: { user_id: req.params.id } });
|
||||||
|
|
||||||
await mdl_UserSessions.update(
|
await mdl_UserSessions.update(
|
||||||
{ is_active: false, logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id } },
|
{ is_active: false, logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id } },
|
||||||
{ where: { user_id: req.params.id } }
|
{ where: { user_id: req.params.id } }
|
||||||
@@ -226,6 +232,13 @@ exports.bulkDeactivateUsers = async (req, res) => {
|
|||||||
{ where: { user_id: activeIds } }
|
{ where: { user_id: activeIds } }
|
||||||
);
|
);
|
||||||
await mdl_Users.destroy({ where: { user_id: activeIds } });
|
await mdl_Users.destroy({ where: { user_id: activeIds } });
|
||||||
|
|
||||||
|
await mdl_UserGroupMembers.update(
|
||||||
|
{ deletedBy: req.user.user_id },
|
||||||
|
{ where: { user_id: activeIds } }
|
||||||
|
);
|
||||||
|
await mdl_UserGroupMembers.destroy({ where: { user_id: activeIds } });
|
||||||
|
|
||||||
await mdl_UserSessions.update(
|
await mdl_UserSessions.update(
|
||||||
{ is_active: false, logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id } },
|
{ is_active: false, logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id } },
|
||||||
{ where: { user_id: activeIds } }
|
{ where: { user_id: activeIds } }
|
||||||
@@ -310,6 +323,7 @@ exports.bulkRestoreUsers = async (req, res) => {
|
|||||||
// so a user who has ever banned someone can't be nulled out or hard-deleted —
|
// so a user who has ever banned someone can't be nulled out or hard-deleted —
|
||||||
// getBannerBlockedIds() below identifies those and callers must exclude them.
|
// getBannerBlockedIds() below identifies those and callers must exclude them.
|
||||||
async function purgeUserDependents(userIds, t) {
|
async function purgeUserDependents(userIds, t) {
|
||||||
|
await mdl_UserGroupMembers.destroy({ where: { user_id: userIds }, force: true, transaction: t });
|
||||||
await mdl_Achievements.destroy({ where: { user_id: userIds }, transaction: t });
|
await mdl_Achievements.destroy({ where: { user_id: userIds }, transaction: t });
|
||||||
await mdl_QuizAttempt.destroy({ where: { user_id: userIds }, transaction: t });
|
await mdl_QuizAttempt.destroy({ where: { user_id: userIds }, transaction: t });
|
||||||
await mdl_UserTiers.destroy({ where: { user_id: userIds }, transaction: t });
|
await mdl_UserTiers.destroy({ where: { user_id: userIds }, transaction: t });
|
||||||
@@ -458,7 +472,7 @@ exports.terminateSession = async (req, res) => {
|
|||||||
|
|
||||||
exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
|
exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
|
||||||
blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"],
|
blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"],
|
||||||
extraDateFields: ["modifiedAt"],
|
extraDateFields: ["modifiedAt", "ban_expires_at"],
|
||||||
allowJsonb: true,
|
allowJsonb: true,
|
||||||
selfJoin: true,
|
selfJoin: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ const computedAttributes = [
|
|||||||
literal: `(
|
literal: `(
|
||||||
SELECT CAST(COUNT(*) AS INTEGER)
|
SELECT CAST(COUNT(*) AS INTEGER)
|
||||||
FROM "user_group_members"
|
FROM "user_group_members"
|
||||||
|
JOIN "users" ON "users"."user_id" = "user_group_members"."user_id"
|
||||||
WHERE "user_group_members"."group_id" = "UserGroup"."group_id"
|
WHERE "user_group_members"."group_id" = "UserGroup"."group_id"
|
||||||
AND "user_group_members"."deletedAt" IS NULL
|
AND "user_group_members"."deletedAt" IS NULL
|
||||||
|
AND "users"."deletedAt" IS NULL
|
||||||
)`,
|
)`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
+12
-3
@@ -6,7 +6,14 @@
|
|||||||
// deleteFile(key)
|
// deleteFile(key)
|
||||||
//
|
//
|
||||||
// Required .env vars:
|
// Required .env vars:
|
||||||
// S3_ENDPOINT – http://127.0.0.1:3900 (internal — always used for uploads/deletes)
|
// S3_ENDPOINT – address this backend uses for uploads/deletes.
|
||||||
|
// http://127.0.0.1:3900 when co-located with Garage.
|
||||||
|
// Otherwise (backend runs on a different machine than
|
||||||
|
// Garage) point this at a network-reachable address that
|
||||||
|
// terminates on Garage — e.g. the same tunneled domain as
|
||||||
|
// S3_PUBLIC_URL, since garage-anon-proxy re-signs any
|
||||||
|
// non-presigned request with real credentials before
|
||||||
|
// forwarding. Never leave this blank.
|
||||||
// S3_REGION – garage
|
// S3_REGION – garage
|
||||||
// S3_ACCESS_KEY
|
// S3_ACCESS_KEY
|
||||||
// S3_SECRET_KEY
|
// S3_SECRET_KEY
|
||||||
@@ -28,8 +35,10 @@ const credentials = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Internal client — uploads, deletes, direct streams from the server itself.
|
// Internal client — uploads, deletes, direct streams from the server itself.
|
||||||
// Always targets S3_ENDPOINT: these calls originate from this machine, so the
|
// Always targets S3_ENDPOINT. When this backend is co-located with Garage,
|
||||||
// internal address is the correct (and only) one to use.
|
// that's a local address; when it isn't, S3_ENDPOINT must instead be a
|
||||||
|
// network-reachable address that reaches Garage (e.g. the tunneled proxy
|
||||||
|
// domain) — see the .env docs above. Do not assume co-location here.
|
||||||
const s3 = new S3Client({
|
const s3 = new S3Client({
|
||||||
endpoint: process.env.S3_ENDPOINT,
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
region: process.env.S3_REGION || "garage",
|
region: process.env.S3_REGION || "garage",
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ const getFieldValues = (Model, logTag, options = {}) => async (req, res) => {
|
|||||||
if (!allowedFields.includes(field) && !dateFields.includes(field))
|
if (!allowedFields.includes(field) && !dateFields.includes(field))
|
||||||
return R.error(res, "Invalid or restricted field.", 400);
|
return R.error(res, "Invalid or restricted field.", 400);
|
||||||
|
|
||||||
|
// ─── ENUM / BOOLEAN fields — return the canonical value set, not a DISTINCT
|
||||||
|
// scan of current rows. A DISTINCT query silently omits valid values
|
||||||
|
// that just don't happen to exist yet (e.g. is_banned when nobody is
|
||||||
|
// currently banned), which produces an incomplete filter picklist.
|
||||||
|
const rawType = Model.rawAttributes[field]?.type;
|
||||||
|
const typeKey = rawType?.constructor?.key || rawType?.key;
|
||||||
|
if (typeKey === "ENUM") {
|
||||||
|
return R.success(res, "Field values retrieved.", rawType.values ?? []);
|
||||||
|
}
|
||||||
|
if (typeKey === "BOOLEAN") {
|
||||||
|
return R.success(res, "Field values retrieved.", ["true", "false"]);
|
||||||
|
}
|
||||||
|
|
||||||
if (auditByFields.includes(field)) {
|
if (auditByFields.includes(field)) {
|
||||||
// Filtering must match the audit column's real (bigint) id — returning
|
// Filtering must match the audit column's real (bigint) id — returning
|
||||||
// just the display name here previously made buildWhere() compare a
|
// just the display name here previously made buildWhere() compare a
|
||||||
|
|||||||
@@ -95,6 +95,15 @@ async function paginate(model, req, {
|
|||||||
const jsonbExclude = excludeAttributes.filter((f) => f.includes('.'));
|
const jsonbExclude = excludeAttributes.filter((f) => f.includes('.'));
|
||||||
const jsonbAttr = jsonbColumn ? excludeJsonbPaths(jsonbColumn, jsonbExclude) : null;
|
const jsonbAttr = jsonbColumn ? excludeJsonbPaths(jsonbColumn, jsonbExclude) : null;
|
||||||
|
|
||||||
|
// The stripped jsonbAttr projection is aliased to the same name as the raw
|
||||||
|
// column (e.g. "personal_info") — without excluding the raw column too, it
|
||||||
|
// gets selected twice under the same alias, which Postgres/CockroachDB
|
||||||
|
// accept in a plain SELECT but reject as ambiguous the moment that alias is
|
||||||
|
// referenced in ORDER BY (e.g. sorting a jsonb-path column).
|
||||||
|
if (jsonbAttr && jsonbColumn && !topLevelExclude.includes(jsonbColumn)) {
|
||||||
|
topLevelExclude.push(jsonbColumn);
|
||||||
|
}
|
||||||
|
|
||||||
const attributes = modelToAttributes(model, { exclude: excludeAttributes, jsonbSchemas, context });
|
const attributes = modelToAttributes(model, { exclude: excludeAttributes, jsonbSchemas, context });
|
||||||
const ALLOWED_FIELDS = attributes.map((a) => a.field);
|
const ALLOWED_FIELDS = attributes.map((a) => a.field);
|
||||||
const computedFieldKeys = computedAttributes.map((c) => c.key);
|
const computedFieldKeys = computedAttributes.map((c) => c.key);
|
||||||
|
|||||||
Reference in New Issue
Block a user