Skip to content

Commit

Permalink
Merge pull request #297 from shaunwarman/feat/admin-add-alias-count
Browse files Browse the repository at this point in the history
feat: add alias and domain counts to respective admin pages
  • Loading branch information
titanism authored Oct 3, 2024
2 parents b6bf01b + 8802219 commit 18408e0
Show file tree
Hide file tree
Showing 32 changed files with 224 additions and 25 deletions.
5 changes: 5 additions & 0 deletions app/models/domains.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ const Domains = new mongoose.Schema({
default: '25',
validator: (value) => isPort(value)
},
alias_count: {
type: Number,
min: 0,
index: true
},
members: [Member],
invites: [Invite],
name: {
Expand Down
6 changes: 6 additions & 0 deletions app/models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ object[config.userFields.defaultDomain] = {
ref: 'Domains'
};

object[config.userFields.domainCount] = {
type: Number,
min: 0,
index: true
};

// Rate limit whitelisting
// TODO: change to allowlist
object[config.userFields.isRateLimitWhitelisted] = {
Expand Down
3 changes: 3 additions & 0 deletions app/views/admin/domains/_table.pug
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ include ../../_pagination
th(scope="col")
+sortHeader('is_global', 'Global', '#table-domains')
th.align-middle(scope="col")= t("Members")
th(scope="col")
+sortHeader('alias_count', 'Aliases', '#table-domains')
th(scope="col")
+sortHeader('plan', 'Plan', '#table-domains')
th(scope="col")
Expand Down Expand Up @@ -52,6 +54,7 @@ include ../../_pagination
= "("
code.text-muted= member.user.id
= ")"
td.align-middle.text-center= domain.alias_count
td.align-middle.text-center
= t(titleize(humanize(domain.plan)))
td.align-middle.text-center
Expand Down
3 changes: 3 additions & 0 deletions app/views/admin/users/_table.pug
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ include ../../_pagination
+sortHeader(config.passport.fields.familyName, 'Last Name', '#table-users')
th(scope="col")
+sortHeader('email', null, '#table-users')
th(scope="col")
+sortHeader('domain_count', 'Domains', '#table-users')
th(scope="col")
+sortHeader('plan', null, '#table-users')
th(scope="col")
Expand Down Expand Up @@ -43,6 +45,7 @@ include ../../_pagination
= "("
code.text-muted= user.id
= ")"
td.align-middle= user.domain_count
td.align-middle= titleize(humanize(user.plan))
td.align-middle= titleize(humanize(user.group))
td.align-middle.dayjs(
Expand Down
1 change: 1 addition & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ const config = {
paypalPayerID: 'paypal_payer_id',
paypalSubscriptionID: 'paypal_subscription_id',
defaultDomain: 'default_domain',
domainCount: 'domain_count',
companyName: 'company_name',
addressLine1: 'address_line1',
addressLine2: 'address_line2',
Expand Down
6 changes: 6 additions & 0 deletions jobs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ const jobs = [
name: 'past-due-relief',
interval: '1h',
timeout: 0
},
// aggregate alias_count and domain_count each day
{
name: 'update-alias-and-domain-counts',
interval: '1d',
timeout: 0
}
];

Expand Down
139 changes: 139 additions & 0 deletions jobs/update-alias-and-domain-counts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* Copyright (c) Forward Email LLC
* SPDX-License-Identifier: BUSL-1.1
*/

// eslint-disable-next-line import/no-unassigned-import
require('#config/env');

const process = require('node:process');
const { parentPort } = require('node:worker_threads');

// eslint-disable-next-line import/no-unassigned-import
require('#config/mongoose');

const Graceful = require('@ladjs/graceful');
const mongoose = require('mongoose');

const Domains = require('#models/domains');
const Users = require('#models/users');
const logger = require('#helpers/logger');
const setupMongoose = require('#helpers/setup-mongoose');
const monitorServer = require('#helpers/monitor-server');

monitorServer();

const graceful = new Graceful({
mongooses: [mongoose],
logger
});

graceful.listen();

(async () => {
await setupMongoose(logger);

try {
// aggregating users and calculating domain count
const usersWithDomainCount = await Users.aggregate([
{
$match: {}
},
{
$lookup: {
from: 'domains',
let: { userId: '$_id' },
pipeline: [
{
$match: {
$expr: {
$in: ['$$userId', '$members.user']
}
}
},
{
$addFields: {
adminMembers: {
$filter: {
input: '$members',
as: 'member',
cond: {
$and: [
{ $eq: ['$$member.user', '$$userId'] },
{ $eq: ['$$member.group', 'admin'] }
]
}
}
}
}
},
{
$match: {
adminMembers: { $ne: [] }
}
}
],
as: 'domainsWithAdminMember'
}
},
{
$addFields: {
domain_count: { $size: '$domainsWithAdminMember' }
}
}
]);

// bulk update users with domain_count
if (usersWithDomainCount.length > 0) {
const bulkUserOperations = usersWithDomainCount.map((user) => ({
updateOne: {
filter: { _id: user._id },
update: { $set: { domain_count: user.domain_count } }
}
}));

await Users.bulkWrite(bulkUserOperations);
}

// aggregating domains and calculating alias count
const domainsWithAliases = await Domains.aggregate([
{
$lookup: {
from: 'aliases',
localField: '_id',
foreignField: 'domain',
as: 'matchingAliases'
}
},
{
$addFields: {
alias_count: { $size: '$matchingAliases' }
}
},
{
$project: {
_id: 1,
domain_name: 1,
alias_count: 1
}
}
]);

// bulk update domains with alias_count
if (domainsWithAliases.length > 0) {
const bulkDomainOperations = domainsWithAliases.map((domain) => ({
updateOne: {
filter: { _id: domain._id },
update: { $set: { alias_count: domain.alias_count } }
}
}));

await Domains.bulkWrite(bulkDomainOperations);
}
} catch (err) {
await logger.error(err);
}

if (parentPort) parentPort.postMessage('done');
else process.exit(0);
})();
3 changes: 2 additions & 1 deletion locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -10334,5 +10334,6 @@
"bytes": "بايتات",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "الحد الأقصى لحصة التخزين لهذا الاسم المستعار. اتركه فارغًا لإعادة تعيين الحد الأقصى لحصة المجال الحالية أو أدخل قيمة مثل \"1 جيجابايت\" التي سيتم تحليلها بواسطة",
". This value can only be adjusted by domain admins.": "لا يمكن تعديل هذه القيمة إلا بواسطة مسؤولي المجال.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "تم حذف التقويم المسمى <span class=\"notranslate\">%s</span> بنجاح مع <span class=\"notranslate\">%d</span> حدث (مرفق نسخة احتياطية في حالة وقوع هذا الحادث)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "تم حذف التقويم المسمى <span class=\"notranslate\">%s</span> بنجاح مع <span class=\"notranslate\">%d</span> حدث (مرفق نسخة احتياطية في حالة وقوع هذا الحادث)",
"domains": "domains"
}
3 changes: 2 additions & 1 deletion locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -10334,5 +10334,6 @@
"bytes": "bajtů",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Maximální kvóta úložiště pro tento alias. Ponechte prázdné, chcete-li obnovit aktuální maximální kvótu domény, nebo zadejte hodnotu, například „1 GB“, podle které bude analyzován",
". This value can only be adjusted by domain admins.": ". Tuto hodnotu mohou upravit pouze správci domény.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Kalendář s názvem <span class=\"notranslate\">%s</span> byl úspěšně smazán s <span class=\"notranslate\">%d</span> událostmi (v příloze je záloha pro případ, že by to byla nehoda)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Kalendář s názvem <span class=\"notranslate\">%s</span> byl úspěšně smazán s <span class=\"notranslate\">%d</span> událostmi (v příloze je záloha pro případ, že by to byla nehoda)",
"domains": "domains"
}
3 changes: 2 additions & 1 deletion locales/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -7322,5 +7322,6 @@
"bytes": "bytes",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Maksimal lagringskvote for dette alias. Lad stå tomt for at nulstille til domænets nuværende maksimale kvote, eller indtast en værdi såsom \"1 GB\", der vil blive parset af",
". This value can only be adjusted by domain admins.": ". Denne værdi kan kun justeres af domæneadministratorer.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Kalenderen med navnet <span class=\"notranslate\">%s</span> blev slettet med <span class=\"notranslate\">%d</span> begivenheder (vedhæftet er en sikkerhedskopi, hvis dette var et uheld)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Kalenderen med navnet <span class=\"notranslate\">%s</span> blev slettet med <span class=\"notranslate\">%d</span> begivenheder (vedhæftet er en sikkerhedskopi, hvis dette var et uheld)",
"domains": "domains"
}
3 changes: 2 additions & 1 deletion locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -9373,5 +9373,6 @@
"bytes": "Bytes",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Maximales Speicherkontingent für diesen Alias. Lassen Sie das Feld leer, um das aktuelle maximale Kontingent der Domäne zurückzusetzen, oder geben Sie einen Wert wie „1 GB“ ein, der von",
". This value can only be adjusted by domain admins.": ". Dieser Wert kann nur von Domänenadministratoren angepasst werden.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Der Kalender mit dem Namen <span class=\"notranslate\">%s</span> wurde mit <span class=\"notranslate\">%d</span> Ereignissen erfolgreich gelöscht (anbei eine Sicherungskopie für den Fall, dass dies ein Versehen war)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Der Kalender mit dem Namen <span class=\"notranslate\">%s</span> wurde mit <span class=\"notranslate\">%d</span> Ereignissen erfolgreich gelöscht (anbei eine Sicherungskopie für den Fall, dass dies ein Versehen war)",
"domains": "domains"
}
14 changes: 13 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -6714,5 +6714,17 @@
"bytes": "bytes",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by",
". This value can only be adjusted by domain admins.": ". This value can only be adjusted by domain admins.",
"Private Business Email Service": "Private Business Email Service"
"Private Business Email Service": "Private Business Email Service",
"You do not belong to the administrative user group.": "You do not belong to the administrative user group.",
"Admin - Users": "Admin - Users",
"Search for users": "Search for users",
"Search by first name, last name, or email": "Search by first name, last name, or email",
"First Name": "First Name",
"Last Name": "Last Name",
"email": "email",
"domains": "domains",
"plan": "plan",
"group": "group",
"Last Login": "Last Login",
"Log in as user": "Log in as user"
}
3 changes: 2 additions & 1 deletion locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -10332,5 +10332,6 @@
"bytes": "bytes",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Cuota máxima de almacenamiento para este alias. Déjelo en blanco para restablecer la cuota máxima actual del dominio o ingrese un valor como \"1 GB\" que será analizado por",
". This value can only be adjusted by domain admins.": "Este valor sólo puede ser ajustado por los administradores del dominio.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "El calendario llamado <span class=\"notranslate\">%s</span> se eliminó correctamente con <span class=\"notranslate\">%d</span> eventos (se adjunta una copia de seguridad en caso de que esto haya sido un accidente)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "El calendario llamado <span class=\"notranslate\">%s</span> se eliminó correctamente con <span class=\"notranslate\">%d</span> eventos (se adjunta una copia de seguridad en caso de que esto haya sido un accidente)",
"domains": "domains"
}
3 changes: 2 additions & 1 deletion locales/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10181,5 +10181,6 @@
"bytes": "tavua",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Tämän aliaksen tallennustilan enimmäiskiintiö. Jätä tyhjäksi palauttaaksesi verkkotunnuksen nykyisen enimmäiskiintiön tai anna arvo, kuten \"1 Gt\", jonka jäsentää",
". This value can only be adjusted by domain admins.": ". Vain verkkotunnuksen järjestelmänvalvojat voivat säätää tätä arvoa.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Kalenteri nimeltä <span class=\"notranslate\">%s</span> poistettiin onnistuneesti <span class=\"notranslate\">%d</span> tapahtumalla (liitteenä varmuuskopio siltä varalta, että kyseessä oli onnettomuus)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Kalenteri nimeltä <span class=\"notranslate\">%s</span> poistettiin onnistuneesti <span class=\"notranslate\">%d</span> tapahtumalla (liitteenä varmuuskopio siltä varalta, että kyseessä oli onnettomuus)",
"domains": "domains"
}
3 changes: 2 additions & 1 deletion locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -7859,5 +7859,6 @@
"bytes": "octets",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Quota de stockage maximal pour cet alias. Laissez ce champ vide pour réinitialiser le quota maximal actuel du domaine ou saisissez une valeur telle que « 1 Go » qui sera analysée par",
". This value can only be adjusted by domain admins.": "Cette valeur ne peut être ajustée que par les administrateurs de domaine.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Le calendrier nommé <span class=\"notranslate\">%s</span> a été supprimé avec succès avec <span class=\"notranslate\">%d</span> événements (une sauvegarde est jointe au cas où il s&#39;agirait d&#39;un accident)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "Le calendrier nommé <span class=\"notranslate\">%s</span> a été supprimé avec succès avec <span class=\"notranslate\">%d</span> événements (une sauvegarde est jointe au cas où il s&#39;agirait d&#39;un accident)",
"domains": "domains"
}
3 changes: 2 additions & 1 deletion locales/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -8355,5 +8355,6 @@
"bytes": "בתים",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "מכסת אחסון מקסימלית עבור הכינוי הזה. השאר ריק כדי לאפס למכסה המקסימלית הנוכחית של הדומיין או הזן ערך כגון \"1 GB\" שינתח על ידי",
". This value can only be adjusted by domain admins.": ". ערך זה יכול להיות מותאם רק על ידי מנהלי דומיין.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "לוח שנה בשם <span class=\"notranslate\">%s</span> נמחק בהצלחה עם <span class=\"notranslate\">%d</span> אירועים (מצורף גיבוי למקרה שזו הייתה תאונה)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "לוח שנה בשם <span class=\"notranslate\">%s</span> נמחק בהצלחה עם <span class=\"notranslate\">%d</span> אירועים (מצורף גיבוי למקרה שזו הייתה תאונה)",
"domains": "domains"
}
3 changes: 2 additions & 1 deletion locales/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -10334,5 +10334,6 @@
"bytes": "bájtok",
"Storage maximum quota for this alias. Leave blank to reset to domain current maximum quota or enter a value such as \"1 GB\" that will be parsed by": "Maximális tárhelykvóta ehhez az aliashoz. Hagyja üresen a domain jelenlegi maximális kvótájának visszaállításához, vagy adjon meg egy értéket, például \"1 GB\", amelyet a rendszer elemezni fog",
". This value can only be adjusted by domain admins.": ". Ezt az értéket csak a domain rendszergazdái módosíthatják.",
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "A <span class=\"notranslate\">%s</span> nevű naptár sikeresen törölve <span class=\"notranslate\">%d</span> eseménnyel (mellékelve egy biztonsági másolat arra az esetre, ha baleset történt volna)"
"Calendar named <span class=\"notranslate\">%s</span> was successfully deleted with <span class=\"notranslate\">%d</span> events (attached is a backup in case this was an accident)": "A <span class=\"notranslate\">%s</span> nevű naptár sikeresen törölve <span class=\"notranslate\">%d</span> eseménnyel (mellékelve egy biztonsági másolat arra az esetre, ha baleset történt volna)",
"domains": "domains"
}
Loading

0 comments on commit 18408e0

Please sign in to comment.