Skip to content

Commit eb365a6

Browse files
authored
Merge pull request #635 from codescalers/development_send_email_per_user
support sending emails per users - fix new lines in email and announcement body
2 parents fcd7313 + 10d4f4b commit eb365a6

6 files changed

Lines changed: 170 additions & 14 deletions

File tree

client/src/components/UserInfo.vue

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,49 @@
66
<v-container>
77
<v-row dense>
88
<v-col cols="12" sm="7">
9-
<v-card color="white" theme="dark" flat>
9+
<v-card color="white" flat>
1010
<div
1111
class="d-flex flex-column justify-space-between text-subtitle-1 font-weight-regular secondary"
1212
>
1313
<p><strong>Name: </strong>{{ user.name }}</p>
14-
1514
<p><strong>College: </strong>{{ user.college }}</p>
15+
<p><strong>Project description: </strong>{{ user.project_desc }}</p>
16+
17+
<div>
18+
<v-dialog v-model="emailDialog" width="40%">
19+
<template v-slot:activator="{ props }">
20+
<BaseButton
21+
class="bg-primary text-lowercase my-5"
22+
:icon="'fa-envelope'"
23+
:text="user.email"
24+
v-bind="props"
25+
>
26+
</BaseButton>
27+
</template>
28+
29+
<v-card class="pa-5">
30+
<v-form @submit.prevent="sendEmail(user.email)" ref="form">
31+
<v-card-text>
32+
<v-text-field label="Email" :value="user.email" bg-color="accent"
33+
variant="outlined" density="compact"
34+
class="my-3" disabled focused></v-text-field>
1635

17-
<p>
18-
<strong>Project description: </strong>{{ user.project_desc }}
19-
</p>
36+
<v-text-field label="Subject" v-model="subject" :rules="requiredRules"
37+
oninput="validity.valid||(value='')" bg-color="accent" variant="outlined" density="compact"
38+
class="my-3"></v-text-field>
2039

21-
<BaseButton
22-
class="bg-primary text-lowercase my-5"
23-
:icon="'fa-envelope'"
24-
:text="user.email"
25-
:href="`mailto: ${user.email}`"
26-
>
27-
</BaseButton>
40+
<v-textarea clearable label="Body" v-model="emailBody" :rules="requiredRules"
41+
oninput="validity.valid||(value='')" bg-color="accent" variant="outlined" density="compact"
42+
class="my-3"></v-textarea>
43+
</v-card-text>
44+
<v-card-actions class="justify-center">
45+
<BaseButton class="bg-primary mr-5" text="Cancel" @click="emailDialog = false" />
46+
<BaseButton type="submit" class="bg-primary" text="Send" />
47+
</v-card-actions>
48+
</v-form>
49+
</v-card>
50+
</v-dialog>
51+
</div>
2852
</div>
2953
</v-card>
3054
</v-col>
@@ -59,21 +83,81 @@
5983
</v-card>
6084
</v-col>
6185
</v-row>
86+
<Toast ref="toast" />
6287
</v-container>
6388
</v-card>
6489
</template>
6590

6691
<script>
92+
import {ref, watch } from "vue";
6793
import BaseButton from "@/components/Form/BaseButton.vue";
94+
import userService from "@/services/userService.js";
95+
import Toast from "@/components/Toast.vue";
6896
6997
export default {
7098
components: {
7199
BaseButton,
100+
Toast,
72101
},
73102
props: {
74103
user: {
75104
type: Object,
76105
},
77106
},
107+
setup() {
108+
const form = ref(null);
109+
const toast = ref(null);
110+
const emailDialog = ref(false);
111+
const emailBody = ref(null);
112+
const subject = ref(null);
113+
114+
const requiredRules = ref([
115+
(value) => {
116+
if (value === '') return "Field is required";
117+
return true;
118+
},
119+
]);
120+
121+
const sendEmail = async (userEmail) => {
122+
var { valid } = await form.value.validate();
123+
if (!valid) return;
124+
125+
userService
126+
.sendEmail(subject.value, emailBody.value, userEmail)
127+
.then((response) => {
128+
const { msg } = response.data;
129+
toast.value.toast(msg, "#388E3C");
130+
})
131+
.catch((response) => {
132+
toast.value.toast(response.response.data.err, "#FF5252");
133+
})
134+
.finally(() => {
135+
emailDialog.value = false;
136+
});
137+
};
138+
139+
watch(emailDialog, (val) => {
140+
if (val) {
141+
emailBody.value = "";
142+
subject.value = "";
143+
}
144+
});
145+
146+
return {
147+
form,
148+
toast,
149+
subject,
150+
emailDialog,
151+
emailBody,
152+
requiredRules,
153+
sendEmail
154+
}
155+
},
78156
};
79157
</script>
158+
159+
<style scoped>
160+
[class*="--disabled "] * {
161+
opacity: 1;
162+
}
163+
</style>

client/src/services/userService.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ export default {
170170
return await authClient().post("/announcement", { subject, announcement });
171171
},
172172

173+
// email
174+
async sendEmail(subject, body, email) {
175+
await this.refresh_token();
176+
return await authClient().post("/email", { subject, body, email });
177+
},
178+
173179
async setAdmin(email, admin) {
174180
await this.refresh_token();
175181
return await authClient().put("/set_admin", { email, admin });

server/app/admin_handler.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ type AdminAnnouncement struct {
2222
Body string `json:"announcement" binding:"required"`
2323
}
2424

25+
// EmailUser struct for data needed when admin sends new email to a user
26+
type EmailUser struct {
27+
Subject string `json:"subject" binding:"required"`
28+
Body string `json:"body" binding:"required"`
29+
Email string `json:"email" binding:"required" validate:"mail"`
30+
}
31+
2532
// UpdateMaintenanceInput struct for data needed when user update maintenance
2633
type UpdateMaintenanceInput struct {
2734
ON bool `json:"on" binding:"required"`
@@ -419,6 +426,53 @@ func (a *App) CreateNewAnnouncement(req *http.Request) (interface{}, Response) {
419426
}, Created()
420427
}
421428

429+
// SendEmail creates a new administrator email and sends it to a specific user as an email and notification
430+
func (a *App) SendEmail(req *http.Request) (interface{}, Response) {
431+
var emailUser EmailUser
432+
err := json.NewDecoder(req.Body).Decode(&emailUser)
433+
434+
if err != nil {
435+
log.Error().Err(err).Send()
436+
return nil, BadRequest(errors.New("failed to read email data"))
437+
}
438+
439+
err = validator.Validate(emailUser)
440+
if err != nil {
441+
log.Error().Err(err).Send()
442+
return nil, BadRequest(errors.New("invalid email data"))
443+
}
444+
445+
user, err := a.db.GetUserByEmail(emailUser.Email)
446+
if err == gorm.ErrRecordNotFound {
447+
log.Error().Err(err).Send()
448+
return nil, BadRequest(errors.New("user is not found"))
449+
}
450+
451+
if err != nil {
452+
log.Error().Err(err).Send()
453+
return nil, BadRequest(errors.New("failed to get user"))
454+
}
455+
456+
subject, body := internal.AdminMailContent(emailUser.Subject, emailUser.Body, a.config.Server.Host, user.Name)
457+
458+
err = internal.SendMail(a.config.MailSender.Email, a.config.MailSender.SendGridKey, user.Email, subject, body)
459+
if err != nil {
460+
log.Error().Err(err).Send()
461+
return nil, InternalServerError(errors.New(internalServerErrorMsg))
462+
}
463+
464+
notification := models.Notification{UserID: user.ID.String(), Msg: fmt.Sprintf("Email: %s", emailUser.Body)}
465+
err = a.db.CreateNotification(&notification)
466+
if err != nil {
467+
log.Error().Err(err).Send()
468+
return nil, InternalServerError(errors.New(internalServerErrorMsg))
469+
}
470+
471+
return ResponseMsg{
472+
Message: "new email is sent successfully",
473+
}, Created()
474+
}
475+
422476
// UpdateNextLaunchHandler updates next launch flag
423477
func (a *App) UpdateNextLaunchHandler(req *http.Request) (interface{}, Response) {
424478
var input UpdateNextLaunchInput

server/app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ func (a *App) registerHandlers() {
159159
adminRouter.HandleFunc("/quota/reset", WrapFunc(a.ResetUsersQuota)).Methods("PUT", "OPTIONS")
160160
adminRouter.HandleFunc("/deployment/count", WrapFunc(a.GetDlsCountHandler)).Methods("GET", "OPTIONS")
161161
adminRouter.HandleFunc("/announcement", WrapFunc(a.CreateNewAnnouncement)).Methods("POST", "OPTIONS")
162+
adminRouter.HandleFunc("/email", WrapFunc(a.SendEmail)).Methods("POST", "OPTIONS")
162163
adminRouter.HandleFunc("/set_admin", WrapFunc(a.SetAdmin)).Methods("PUT", "OPTIONS")
163164
balanceRouter.HandleFunc("", WrapFunc(a.GetBalanceHandler)).Methods("GET", "OPTIONS")
164165
maintenanceRouter.HandleFunc("", WrapFunc(a.UpdateMaintenanceHandler)).Methods("PUT", "OPTIONS")

server/internal/email_sender.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,18 @@ func AdminAnnouncementMailContent(adminSubject, announcement, host, username str
144144
subject := "New Announcement! 📢 " + adminSubject
145145
body := string(adminAnnouncement)
146146
body = strings.ReplaceAll(body, "-subject-", adminSubject)
147-
body = strings.ReplaceAll(body, "-announcement-", announcement)
147+
body = strings.ReplaceAll(body, "-announcement-", strings.ReplaceAll(announcement, "\n", "<br>"))
148+
body = strings.ReplaceAll(body, "-name-", cases.Title(language.Und).String(username))
149+
body = strings.ReplaceAll(body, "-host-", host)
150+
return subject, body
151+
}
152+
153+
// AdminMailContent gets the email content for administrator emails
154+
func AdminMailContent(adminSubject, email, host, username string) (string, string) {
155+
subject := "Hey! 📢 " + adminSubject
156+
body := string(adminAnnouncement)
157+
body = strings.ReplaceAll(body, "-subject-", adminSubject)
158+
body = strings.ReplaceAll(body, "-announcement-", strings.ReplaceAll(email, "\n", "<br>"))
148159
body = strings.ReplaceAll(body, "-name-", cases.Title(language.Und).String(username))
149160
body = strings.ReplaceAll(body, "-host-", host)
150161
return subject, body

server/internal/templates/adminAnnouncement.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
line-height: 48px;
172172
"
173173
>
174-
New Announcement! 📢 -subject-
174+
-subject-
175175
</h1>
176176
</td>
177177
</tr>

0 commit comments

Comments
 (0)