Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 843f51a

Browse files
committed
feat(backend): add email notifications for global admin role changes
1 parent cacd443 commit 843f51a

File tree

6 files changed

+274
-0
lines changed

6 files changed

+274
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//- @description Email notification when a user's global administrator role is removed
2+
//- @variables userName, newRoleName, changedByName, supportEmail
3+
extends layouts/base.pug
4+
5+
block content
6+
h1 Your Role Has Been Updated
7+
8+
p Hi #{userName},
9+
10+
p #{changedByName} changed your role from Global Administrator to #{newRoleName}.
11+
12+
.role-info
13+
p
14+
strong Previous Role:
15+
| Global Administrator
16+
17+
p
18+
strong New Role:
19+
| #{newRoleName}
20+
21+
h2 What changed
22+
23+
p Your access level has been adjusted. You may have reduced permissions for certain platform operations.
24+
25+
if supportEmail
26+
p
27+
| If you believe this change was made in error, please contact support at 
28+
a(href=`mailto:${supportEmail}`)= supportEmail
29+
| .
30+
31+
p.text-muted
32+
| Best regards,
33+
br
34+
| The DeployStack Team
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//- @description Email notification when a user is promoted to global administrator
2+
//- @variables userName, promotedByName, dashboardUrl
3+
extends layouts/base.pug
4+
5+
block content
6+
h1 You're now a Global Administrator
7+
8+
p Hi #{userName},
9+
10+
p #{promotedByName} just made you a global administrator. You now have full access to manage users, teams, and all platform settings.
11+
12+
h2 What this means
13+
14+
p As a global admin, you can:
15+
16+
ul
17+
li Manage all users and their roles
18+
li Access the admin dashboard
19+
li Configure platform settings
20+
li View system logs and analytics
21+
22+
if dashboardUrl
23+
.text-center
24+
a.button(href=dashboardUrl) Go to Admin Dashboard
25+
26+
p Questions? Just reach out to your team.
27+
28+
p.text-muted
29+
| Best regards,
30+
br
31+
| The DeployStack Team
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//- @description Email notification to other admins when a user's global administrator role is removed
2+
//- @variables adminName, demotedUserName, newRoleName, changedByName, dashboardUrl
3+
extends layouts/base.pug
4+
5+
block content
6+
h1 Global Administrator Removed
7+
8+
p Hi #{adminName},
9+
10+
p #{changedByName} changed #{demotedUserName}'s role from Global Administrator to #{newRoleName}.
11+
12+
.admin-info
13+
p
14+
strong User:
15+
| #{demotedUserName}
16+
17+
p
18+
strong Changed By:
19+
| #{changedByName}
20+
21+
p
22+
strong Previous Role:
23+
| Global Administrator
24+
25+
p
26+
strong New Role:
27+
| #{newRoleName}
28+
29+
p This user no longer has global administrator access.
30+
31+
if dashboardUrl
32+
.text-center
33+
a.button(href=dashboardUrl) View User Management
34+
35+
p.text-muted
36+
| Best regards,
37+
br
38+
| The DeployStack Team
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//- @description Email notification to other admins when a user is promoted to global administrator
2+
//- @variables adminName, promotedUserName, promotedByName, dashboardUrl
3+
extends layouts/base.pug
4+
5+
block content
6+
h1 New Global Administrator Added
7+
8+
p Hi #{adminName},
9+
10+
p #{promotedByName} just added #{promotedUserName} as a global administrator.
11+
12+
.admin-info
13+
p
14+
strong User Promoted:
15+
| #{promotedUserName}
16+
17+
p
18+
strong Promoted By:
19+
| #{promotedByName}
20+
21+
p
22+
strong New Role:
23+
| Global Administrator
24+
25+
p This user now has full access to manage users, teams, and platform settings.
26+
27+
if dashboardUrl
28+
.text-center
29+
a.button(href=dashboardUrl) View User Management
30+
31+
p.text-muted
32+
| Best regards,
33+
br
34+
| The DeployStack Team

services/backend/src/email/types.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,34 @@ export interface TeamDeletedEmailVariables {
163163
teamName: string;
164164
}
165165

166+
export interface GlobalAdminPromotedEmailVariables {
167+
userName: string;
168+
promotedByName: string;
169+
dashboardUrl?: string;
170+
}
171+
172+
export interface GlobalAdminDemotedEmailVariables {
173+
userName: string;
174+
newRoleName: string;
175+
changedByName: string;
176+
supportEmail?: string;
177+
}
178+
179+
export interface GlobalAdminUserPromotedNotificationVariables {
180+
adminName: string;
181+
promotedUserName: string;
182+
promotedByName: string;
183+
dashboardUrl?: string;
184+
}
185+
186+
export interface GlobalAdminUserDemotedNotificationVariables {
187+
adminName: string;
188+
demotedUserName: string;
189+
newRoleName: string;
190+
changedByName: string;
191+
dashboardUrl?: string;
192+
}
193+
166194
// Template registry for type safety
167195
export interface TemplateVariableMap {
168196
welcome: WelcomeEmailVariables;
@@ -175,6 +203,10 @@ export interface TemplateVariableMap {
175203
'mcp-installation-deleted': McpInstallationDeletedEmailVariables;
176204
'team-created': TeamCreatedEmailVariables;
177205
'team-deleted': TeamDeletedEmailVariables;
206+
'global-admin-promoted': GlobalAdminPromotedEmailVariables;
207+
'global-admin-demoted': GlobalAdminDemotedEmailVariables;
208+
'global-admin-user-promoted-notification': GlobalAdminUserPromotedNotificationVariables;
209+
'global-admin-user-demoted-notification': GlobalAdminUserDemotedNotificationVariables;
178210
}
179211

180212
export type TemplateNames = keyof TemplateVariableMap;

services/backend/src/routes/admin/users/assign-role.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ export default async function assignRoleAdminRoute(server: FastifyInstance) {
9696
const { id } = request.params as ParamsWithId;
9797
const { role_id } = request.body as AssignRoleRequest;
9898

99+
// Get user's current role before making changes
100+
const userBeforeChange = await userService.getUserById(id);
101+
if (!userBeforeChange) {
102+
const errorResponse: ErrorResponse = {
103+
success: false,
104+
error: 'User not found'
105+
};
106+
const jsonString = JSON.stringify(errorResponse);
107+
return reply.status(404).type('application/json').send(jsonString);
108+
}
109+
110+
const oldRoleId = userBeforeChange.role_id;
111+
const isPromotionToGlobalAdmin = role_id === 'global_admin' && oldRoleId !== 'global_admin';
112+
const isDemotionFromGlobalAdmin = oldRoleId === 'global_admin' && role_id !== 'global_admin';
113+
99114
// Prevent users from changing their own role
100115
if (request.user?.id === id) {
101116
const errorResponse: ErrorResponse = {
@@ -120,6 +135,96 @@ export default async function assignRoleAdminRoute(server: FastifyInstance) {
120135
// Get updated user data
121136
const user = await userService.getUserById(id);
122137

138+
// Send email notifications for global_admin role changes
139+
if (isPromotionToGlobalAdmin || isDemotionFromGlobalAdmin) {
140+
try {
141+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142+
const jobQueueService = (server as any).jobQueueService;
143+
if (jobQueueService && userBeforeChange.email && request.user) {
144+
const currentUser = request.user as { id: string; username?: string; email?: string };
145+
const actorName = currentUser.username || currentUser.email || 'Administrator';
146+
const targetUserName = userBeforeChange.username || userBeforeChange.email;
147+
148+
// Email 1: Notify the user whose role changed
149+
if (isPromotionToGlobalAdmin) {
150+
await jobQueueService.createJob('send_email', {
151+
to: userBeforeChange.email,
152+
subject: 'You\'ve been promoted to Global Administrator',
153+
template: 'global-admin-promoted',
154+
variables: {
155+
userName: targetUserName,
156+
promotedByName: actorName,
157+
dashboardUrl: process.env.FRONTEND_URL || undefined
158+
}
159+
});
160+
server.log.info({ operation: 'global_admin_promoted', userId: id }, 'Global admin promotion email queued');
161+
} else if (isDemotionFromGlobalAdmin) {
162+
const newRole = await userService.getUserById(id);
163+
const newRoleName = newRole?.role?.name || 'Standard User';
164+
165+
await jobQueueService.createJob('send_email', {
166+
to: userBeforeChange.email,
167+
subject: 'Your role has been updated',
168+
template: 'global-admin-demoted',
169+
variables: {
170+
userName: targetUserName,
171+
newRoleName: newRoleName,
172+
changedByName: actorName,
173+
supportEmail: process.env.SUPPORT_EMAIL || undefined
174+
}
175+
});
176+
server.log.info({ operation: 'global_admin_demoted', userId: id }, 'Global admin demotion email queued');
177+
}
178+
179+
// Email 2: Notify OTHER global admins (exclude actor and target user)
180+
const allGlobalAdmins = await userService.getUsersByRole('global_admin');
181+
const otherAdmins = allGlobalAdmins.filter(admin =>
182+
admin.id !== request.user!.id && admin.email && admin.id !== id
183+
);
184+
185+
if (otherAdmins.length > 0) {
186+
for (const admin of otherAdmins) {
187+
const adminDisplayName = admin.username || admin.email;
188+
189+
if (isPromotionToGlobalAdmin) {
190+
await jobQueueService.createJob('send_email', {
191+
to: admin.email,
192+
subject: 'New Global Administrator Added',
193+
template: 'global-admin-user-promoted-notification',
194+
variables: {
195+
adminName: adminDisplayName,
196+
promotedUserName: targetUserName,
197+
promotedByName: actorName,
198+
dashboardUrl: process.env.FRONTEND_URL || undefined
199+
}
200+
});
201+
} else if (isDemotionFromGlobalAdmin) {
202+
const newRole = await userService.getUserById(id);
203+
const newRoleName = newRole?.role?.name || 'Standard User';
204+
205+
await jobQueueService.createJob('send_email', {
206+
to: admin.email,
207+
subject: 'Global Administrator Removed',
208+
template: 'global-admin-user-demoted-notification',
209+
variables: {
210+
adminName: adminDisplayName,
211+
demotedUserName: targetUserName,
212+
newRoleName: newRoleName,
213+
changedByName: actorName,
214+
dashboardUrl: process.env.FRONTEND_URL || undefined
215+
}
216+
});
217+
}
218+
}
219+
server.log.info({ notifiedCount: otherAdmins.length }, 'Notified other global admins');
220+
}
221+
}
222+
} catch (emailError) {
223+
server.log.error(emailError, 'Failed to queue emails - role change succeeded');
224+
// Don't fail role assignment if email queueing fails
225+
}
226+
}
227+
123228
const successResponse: AssignRoleSuccessResponse = {
124229
success: true,
125230
data: user,

0 commit comments

Comments
 (0)