• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GrottoCenter / grottocenter-api / 26345101095

23 May 2026 10:21PM UTC coverage: 87.168% (+0.1%) from 87.055%
26345101095

Pull #1609

github

ClemRz
feat(auth): harden admin account security with TOTP MFA and brute-force protection

- Reduce admin token TTL to 10 days (non-admin remains 90 days)
- Add mandatory TOTP-based MFA with enrollment, verification, and reset endpoints
- Apply stricter rate limiting for admin login (5 req / 15 min / IP)
- Ban admin accounts after 5 consecutive failed logins or TOTP attempts
- Send email notifications on suspicious login activity and account ban
- Revoke all admin tokens on password change
- Update Swagger documentation with new MFA endpoints and login statuses
Pull Request #1609: feat(auth): harden admin account security with TOTP MFA and brute-force protection

3385 of 4037 branches covered (83.85%)

Branch coverage included in aggregate %.

209 of 223 new or added lines in 10 files covered. (93.72%)

2 existing lines in 2 files now uncovered.

6798 of 7645 relevant lines covered (88.92%)

57.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.1
/api/services/AdminLoginProtectionService.js
1
/**
2
 * AdminLoginProtectionService.js
3
 *
4
 * @description :: Handles consecutive failure tracking, account banning,
5
 *                 and suspicious-activity email notifications for
6
 *                 Administrator accounts. All state is persisted in the
7
 *                 database (stateless service).
8
 */
9

10
const LanguageService = require('./LanguageService');
1✔
11

12
module.exports = {
1✔
13
  /**
14
   * Record a failed login attempt for an admin account.
15
   * Increments the loginFailedAttempts counter, updates lastFailedLoginAt,
16
   * bans the account if the threshold is reached, and sends notification
17
   * emails as appropriate (fire-and-forget).
18
   * @param {Object} caver - The caver record (with groups populated)
19
   * @param {string} ip - Source IP address
20
   * @returns {Promise<{ banned: boolean }>}
21
   */
22
  async recordFailedLogin(caver, ip) {
23
    const threshold = sails.config.custom.adminLoginFailureThreshold || 5;
40!
24
    const suspiciousThreshold =
25
      sails.config.custom.suspiciousActivityEmailThreshold || 3;
40!
26
    const cooldownMs =
27
      sails.config.custom.suspiciousActivityEmailCooldown || 900000;
40!
28

29
    const newCount = (caver.loginFailedAttempts || 0) + 1;
40✔
30
    const banned = newCount >= threshold;
40✔
31
    const now = new Date();
40✔
32

33
    const updateValues = {
40✔
34
      loginFailedAttempts: newCount,
35
      lastFailedLoginAt: now,
36
    };
37

38
    if (banned) {
40✔
39
      updateValues.banned = true;
3✔
40
    }
41

42
    // Check if suspicious-activity email should be sent
43
    if (newCount >= suspiciousThreshold && !banned) {
40✔
44
      const lastEmailAt = caver.lastSuspiciousEmailAt
15✔
45
        ? new Date(caver.lastSuspiciousEmailAt)
46
        : null;
47
      const cooldownExpired =
48
        !lastEmailAt || now.getTime() - lastEmailAt.getTime() >= cooldownMs;
15✔
49

50
      if (cooldownExpired) {
15✔
51
        updateValues.lastSuspiciousEmailAt = now;
5✔
52
        // Fire-and-forget: send suspicious login email
53
        this.sendSuspiciousLoginEmail(caver, {
5✔
54
          failedAttempts: newCount,
55
          lastAttemptTime: now.toISOString(),
56
          sourceIp: ip || 'unknown',
5!
57
        });
58
      }
59
    }
60

61
    await TCaver.updateOne({ id: caver.id }).set(updateValues);
40✔
62

63
    if (banned) {
40✔
64
      sails.log.warn(
3✔
65
        `AdminLoginProtection: account ${caver.id} banned after ${newCount} failed login attempts (IP: ${ip || 'unknown'})`
3!
66
      );
67
      // Fire-and-forget: send ban notification email
68
      this.sendAccountBannedEmail(caver, {
3✔
69
        lastAttemptTime: now.toISOString(),
70
        sourceIp: ip || 'unknown',
3!
71
      });
72
    }
73

74
    return { banned };
40✔
75
  },
76

77
  /**
78
   * Record a failed TOTP attempt for an admin account.
79
   * Increments the totpFailedAttempts counter and bans the account
80
   * if the threshold is reached.
81
   * @param {Object} caver - The caver record
82
   * @returns {Promise<{ banned: boolean }>}
83
   */
84
  async recordFailedTotp(caver) {
85
    const threshold = sails.config.custom.adminTotpFailureThreshold || 5;
7!
86
    const newCount = (caver.totpFailedAttempts || 0) + 1;
7✔
87
    const banned = newCount >= threshold;
7✔
88

89
    const updateValues = {
7✔
90
      totpFailedAttempts: newCount,
91
    };
92

93
    if (banned) {
7✔
94
      updateValues.banned = true;
1✔
95
    }
96

97
    await TCaver.updateOne({ id: caver.id }).set(updateValues);
7✔
98

99
    if (banned) {
7✔
100
      sails.log.warn(
1✔
101
        `AdminLoginProtection: account ${caver.id} banned after ${newCount} failed TOTP attempts`
102
      );
103
    }
104

105
    return { banned };
7✔
106
  },
107

108
  /**
109
   * Reset failure counters on successful login.
110
   * @param {number} caverId
111
   * @returns {Promise<void>}
112
   */
113
  async resetCounters(caverId) {
114
    await TCaver.updateOne({ id: caverId }).set({
62✔
115
      loginFailedAttempts: 0,
116
      totpFailedAttempts: 0,
117
    });
118
  },
119

120
  /**
121
   * Check if an admin account is banned.
122
   * @param {Object} caver - The caver record
123
   * @returns {boolean}
124
   */
125
  isAccountBanned(caver) {
126
    return caver.banned === true;
70✔
127
  },
128

129
  /**
130
   * Send a suspicious-login notification email (fire-and-forget).
131
   * Errors are caught and logged — this method never throws.
132
   * @param {Object} caver - The caver record
133
   * @param {Object} details
134
   * @param {number} details.failedAttempts
135
   * @param {string} details.lastAttemptTime - ISO 8601 UTC
136
   * @param {string} details.sourceIp
137
   */
138
  sendSuspiciousLoginEmail(
139
    caver,
140
    { failedAttempts, lastAttemptTime, sourceIp }
141
  ) {
142
    const doSend = async () => {
5✔
143
      const locale =
144
        (await LanguageService.getLocale(caver.language)) ||
5!
145
        sails.config.i18n.defaultLocale;
146

147
      await sails.helpers.sendEmail.with({
5✔
148
        allowResponse: false,
149
        emailSubject: 'Suspicious Login Activity',
150
        locale,
151
        recipientEmail: caver.mail,
152
        viewName: 'suspiciousLogin',
153
        viewValues: {
154
          recipientName: caver.nickname,
155
          failedAttempts,
156
          lastAttemptTime,
157
          sourceIp,
158
        },
159
      });
160
    };
161

162
    // Fire-and-forget with error logging
163
    doSend().catch((error) => {
5✔
164
      sails.log.error(
1✔
165
        `AdminLoginProtection: failed to send suspicious-login email for caver ${caver.id} at ${new Date().toISOString()}: ${error.message}`
166
      );
167
    });
168
  },
169

170
  /**
171
   * Send an account-banned notification email (fire-and-forget).
172
   * Errors are caught and logged — this method never throws.
173
   * @param {Object} caver - The caver record
174
   * @param {Object} details
175
   * @param {string} details.lastAttemptTime - ISO 8601 UTC
176
   * @param {string} details.sourceIp
177
   */
178
  sendAccountBannedEmail(caver, { lastAttemptTime, sourceIp }) {
179
    const doSend = async () => {
3✔
180
      const locale =
181
        (await LanguageService.getLocale(caver.language)) ||
3!
182
        sails.config.i18n.defaultLocale;
183

184
      await sails.helpers.sendEmail.with({
3✔
185
        allowResponse: false,
186
        emailSubject: 'Account Banned',
187
        locale,
188
        recipientEmail: caver.mail,
189
        viewName: 'accountBanned',
190
        viewValues: {
191
          recipientName: caver.nickname,
192
          lastAttemptTime,
193
          sourceIp,
194
        },
195
      });
196
    };
197

198
    // Fire-and-forget with error logging
199
    doSend().catch((error) => {
3✔
NEW
200
      sails.log.error(
×
201
        `AdminLoginProtection: failed to send account-banned email for caver ${caver.id} at ${new Date().toISOString()}: ${error.message}`
202
      );
203
    });
204
  },
205
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc