diff --git a/dashcaddy-api/__tests__/admin-invites.test.js b/dashcaddy-api/__tests__/admin-invites.test.js index f75aadf..3b8b47a 100644 --- a/dashcaddy-api/__tests__/admin-invites.test.js +++ b/dashcaddy-api/__tests__/admin-invites.test.js @@ -237,4 +237,49 @@ describe('DC-085: link-first admin invites', () => { .send({ email: 'a@x.com', ttlHours: 1 }); expect(res.body.shareText).toContain('expires in 1h'); }); -}); \ No newline at end of file + + test('DC-089: SMTP-unconfigured warn log masks the invite email (no raw PII)', async () => { + mockEmailSender.isConfigured.mockReturnValueOnce(false); + + const res = await request(app) + .post('/api/v1/auth/admin/invites') + .send({ email: 'friend@example.com', role: 'operator', sendEmail: true }); + + expect(res.status).toBe(200); + expect(res.body.deliveredVia).toBe('failed'); + const warn = logCalls.find(c => + c.level === 'warn' && c.topic === 'auth-invite-send' + ); + expect(warn).toBeDefined(); + // The raw address must not appear; the masked form must. + expect(JSON.stringify(warn.meta)).not.toContain('friend@example.com'); + expect(warn.meta.email).toBe('fr****@example.com'); + }); + + test('DC-089: invite-accepted info log masks the created user email (no raw PII)', async () => { + // Pre-authorize the email (POST /admin/users) so userStore.login doesn't + // reject with not_authorized — bootstrap already happened in beforeEach. + const preauth = await request(app) + .post('/api/v1/auth/admin/users') + .send({ email: 'newfriend@example.com' }); + expect(preauth.status).toBe(200); + + const issue = await request(app) + .post('/api/v1/auth/admin/invites') + .send({ email: 'newfriend@example.com', role: 'viewer' }); + expect(issue.status).toBe(200); + const token = issue.body.acceptUrl.match(/invites\/([^/]+)\/accept/)[1]; + + const res = await request(app) + .post(`/api/v1/auth/invites/${token}/accept`) + .send({}); + + expect(res.status).toBe(200); + const info = logCalls.find(c => + c.level === 'info' && c.msg === 'invite accepted, user created' + ); + expect(info).toBeDefined(); + expect(JSON.stringify(info.meta)).not.toContain('newfriend@example.com'); + expect(info.meta.email).toBe('ne****@example.com'); + }); +}); diff --git a/dashcaddy-api/routes/auth/admin.js b/dashcaddy-api/routes/auth/admin.js index 015e00b..cfe6e3e 100644 --- a/dashcaddy-api/routes/auth/admin.js +++ b/dashcaddy-api/routes/auth/admin.js @@ -29,6 +29,7 @@ const platformPaths = require('../../platform-paths'); const { createUserStore } = require('../../src/security/user-store'); const { createInviteStore } = require('../../src/security/invite-store'); const emailSender = require('../../src/auth/providers/email-sender'); +const AuthProvider = require('../../src/auth/providers/base'); const { ValidationError, NotFoundError, ForbiddenError, PaymentRequiredError } = require('../../src/utilities/errors'); const { ok, successMessage } = require('../../src/utils/responses'); @@ -254,7 +255,7 @@ module.exports = function({ asyncHandler, errorResponse, log, session, dataDir } // server log on every unconfigured-install invite. log.warn && log.warn('auth-invite-send', 'invite send skipped: SMTP not configured (operator opted in)', - { inviteId: issued.id, email: issued.email }); + { inviteId: issued.id, email: AuthProvider.maskEmail(issued.email) || '[unmaskable-email]' }); deliveredVia = 'failed'; } } catch (sendErr) { @@ -369,7 +370,7 @@ module.exports = function({ asyncHandler, errorResponse, log, session, dataDir } log.info && log.info('auth', 'invite accepted, user created', { userId: userResult.user.id, - email: userResult.user.email, + email: AuthProvider.maskEmail(userResult.user.email) || '[unmaskable-email]', role: userResult.user.role, inviteId: invite.id, });