From b7d25473c67f324699ebbdbeb5aabbf67347cdee Mon Sep 17 00:00:00 2001 From: NGPixel Date: Mon, 30 Oct 2023 04:43:40 +0000 Subject: [PATCH] feat: user welcome email --- server/graph/schemas/user.graphql | 1 + server/locales/en.json | 2 + server/models/users.mjs | 54 ++++++--- server/templates/mail/UserWelcome.vue | 109 ++++++++++++++++++ .../_assets/icons/ultraviolet-web-design.svg | 1 + ux/src/components/UserCreateDialog.vue | 38 +++++- 6 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 server/templates/mail/UserWelcome.vue create mode 100644 ux/public/_assets/icons/ultraviolet-web-design.svg diff --git a/server/graph/schemas/user.graphql b/server/graph/schemas/user.graphql index 4045b044..a81d86ec 100644 --- a/server/graph/schemas/user.graphql +++ b/server/graph/schemas/user.graphql @@ -36,6 +36,7 @@ extend type Mutation { groups: [UUID]! mustChangePassword: Boolean! sendWelcomeEmail: Boolean! + sendWelcomeEmailFromSiteId: UUID ): UserResponse updateUser( diff --git a/server/locales/en.json b/server/locales/en.json index eb6d5f7d..125cb978 100644 --- a/server/locales/en.json +++ b/server/locales/en.json @@ -919,6 +919,7 @@ "admin.users.create": "Create User", "admin.users.createInvalidData": "Cannot create user as some fields are invalid or missing.", "admin.users.createKeepOpened": "Keep dialog opened after create", + "admin.users.createSendEmailMissingSiteId": "You must specify the wiki site to reference for the welcome email.", "admin.users.createSuccess": "User created successfully!", "admin.users.createdAt": "Created on {date}", "admin.users.darkMode": "Dark Mode", @@ -1001,6 +1002,7 @@ "admin.users.selectGroup": "Select Group...", "admin.users.sendWelcomeEmail": "Send Welcome Email", "admin.users.sendWelcomeEmailAltHint": "An email will be sent to the user with link(s) to the wiki(s) the user has read access to.", + "admin.users.sendWelcomeEmailFromSiteId": "Site to use for the Welcome Email", "admin.users.sendWelcomeEmailHint": "An email will be sent to the user with his login details.", "admin.users.subtitle": "Manage Users", "admin.users.tfa": "Two Factor Authentication (2FA)", diff --git a/server/models/users.mjs b/server/models/users.mjs index e623b932..f405290f 100644 --- a/server/models/users.mjs +++ b/server/models/users.mjs @@ -570,7 +570,7 @@ export class User extends Model { * * @param {Object} param0 User Fields */ - static async createNewUser ({ email, password, name, groups, userInitiated = false, mustChangePassword = false, sendWelcomeEmail = false }) { + static async createNewUser ({ email, password, name, groups, userInitiated = false, mustChangePassword = false, sendWelcomeEmail = false, sendWelcomeEmailFromSiteId }) { const localAuth = await WIKI.db.authentication.getStrategy('local') // Check if self-registration is enabled @@ -630,6 +630,11 @@ export class User extends Model { throw new Error('ERR_DUPLICATE_ACCOUNT_EMAIL') } + // Check if site ID is provided when send welcome email is enabled + if (sendWelcomeEmail && !sendWelcomeEmailFromSiteId) { + throw new Error('ERR_INVALID_SITE') + } + WIKI.logger.debug(`Creating new user account for ${email}...`) // Create the account @@ -681,35 +686,48 @@ export class User extends Model { userId: newUsr.id }) + // TODO: Handle multilingual text // Send verification email await WIKI.mail.send({ - template: 'accountVerify', + template: 'UserVerify', to: email, subject: 'Verify your account', data: { preheadertext: 'Verify your account in order to gain access to the wiki.', title: 'Verify your account', content: 'Click the button below in order to verify your account and gain access to the wiki.', - buttonLink: `${WIKI.config.host}/verify/${verificationToken}`, + buttonLink: `${WIKI.config.mail.defaultBaseURL}/verify/${verificationToken}`, buttonText: 'Verify' }, - text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.host}/verify/${verificationToken}` + text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.mail.defaultBaseURL}/verify/${verificationToken}` }) } else if (sendWelcomeEmail) { - // Send welcome email - await WIKI.mail.send({ - template: 'accountWelcome', - to: email, - subject: `Welcome to the wiki ${WIKI.config.title}`, - data: { - preheadertext: `You've been invited to the wiki ${WIKI.config.title}`, - title: `You've been invited to the wiki ${WIKI.config.title}`, - content: `Click the button below to access the wiki.`, - buttonLink: `${WIKI.config.host}/login`, - buttonText: 'Login' - }, - text: `You've been invited to the wiki ${WIKI.config.title}: ${WIKI.config.host}/login` - }) + // TODO: Handle multilingual text + const site = WIKI.sites[sendWelcomeEmailFromSiteId] + if (site) { + const siteUrl = site.hostname === '*' ? WIKI.config.mail.defaultBaseURL : `https://${site.hostname}` + // Send welcome email + await WIKI.mail.send({ + template: 'UserWelcome', + to: email, + subject: `Welcome to the wiki ${site.config.title}`, + data: { + siteTitle: site.config.title, + preview: `You've been invited to the wiki ${site.config.title}`, + title: `You've been invited to the wiki ${site.config.title}`, + content: `Click the button below to access the wiki.`, + email, + password, + buttonLink: `${siteUrl}/login`, + buttonText: 'Login', + logo: `${siteUrl}/_site/logo` + }, + text: `You've been invited to the wiki ${site.config.title}: ${siteUrl}/login` + }) + } else { + WIKI.logger.warn('An invalid site ID was provided when creating user. No welcome email was sent.') + throw new Error('ERR_INVALID_SITE') + } } WIKI.logger.debug(`Created new user account for ${email} successfully.`) diff --git a/server/templates/mail/UserWelcome.vue b/server/templates/mail/UserWelcome.vue new file mode 100644 index 00000000..4497cf68 --- /dev/null +++ b/server/templates/mail/UserWelcome.vue @@ -0,0 +1,109 @@ + + + diff --git a/ux/public/_assets/icons/ultraviolet-web-design.svg b/ux/public/_assets/icons/ultraviolet-web-design.svg new file mode 100644 index 00000000..6e598c60 --- /dev/null +++ b/ux/public/_assets/icons/ultraviolet-web-design.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/src/components/UserCreateDialog.vue b/ux/src/components/UserCreateDialog.vue index b7df25ac..041af6ee 100644 --- a/ux/src/components/UserCreateDialog.vue +++ b/ux/src/components/UserCreateDialog.vue @@ -134,6 +134,24 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') unchecked-icon='las la-times' :aria-label='t(`admin.users.sendWelcomeEmail`)' ) + q-item(v-if='state.userSendWelcomeEmail') + blueprint-icon(icon='web-design') + q-item-section + q-select( + outlined + :options='adminStore.sites' + v-model='state.userSendWelcomeEmailFromSiteId' + multiple + map-options + emit-value + option-value='id' + option-label='title' + options-dense + dense + hide-bottom-space + :label='t(`admin.users.sendWelcomeEmailFromSiteId`)' + :aria-label='t(`admin.users.sendWelcomeEmailFromSiteId`)' + ) q-card-actions.card-actions q-checkbox( v-model='state.keepOpened' @@ -167,6 +185,8 @@ import { useI18n } from 'vue-i18n' import { useDialogPluginComponent, useQuasar } from 'quasar' import { computed, onMounted, reactive, ref } from 'vue' +import { useAdminStore } from 'src/stores/admin' + // EMITS defineEmits([ @@ -178,6 +198,10 @@ defineEmits([ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent() const $q = useQuasar() +// STORES + +const adminStore = useAdminStore() + // I18N const { t } = useI18n() @@ -191,6 +215,7 @@ const state = reactive({ userGroups: [], userMustChangePassword: false, userSendWelcomeEmail: false, + userSendWelcomeEmailFromSiteId: null, keepOpened: false, groups: [], loadingGroups: false, @@ -299,6 +324,9 @@ async function create () { if (!isFormValid) { throw new Error(t('admin.users.createInvalidData')) } + if (state.userSendWelcomeEmail && !state.userSendWelcomeEmailFromSiteId) { + throw new Error(t('admin.users.createSendEmailMissingSiteId')) + } const resp = await APOLLO_CLIENT.mutate({ mutation: gql` mutation createUser ( @@ -308,6 +336,7 @@ async function create () { $groups: [UUID]! $mustChangePassword: Boolean! $sendWelcomeEmail: Boolean! + $sendWelcomeEmailFromSiteId: UUID ) { createUser ( name: $name @@ -316,6 +345,7 @@ async function create () { groups: $groups mustChangePassword: $mustChangePassword sendWelcomeEmail: $sendWelcomeEmail + sendWelcomeEmailFromSiteId: $sendWelcomeEmailFromSiteId ) { operation { succeeded @@ -330,7 +360,8 @@ async function create () { password: state.userPassword, groups: state.userGroups, mustChangePassword: state.userMustChangePassword, - sendWelcomeEmail: state.userSendWelcomeEmail + sendWelcomeEmail: state.userSendWelcomeEmail, + sendWelcomeEmailFromSiteId: state.userSendWelcomeEmailFromSiteId } }) if (resp?.data?.createUser?.operation?.succeeded) { @@ -360,5 +391,8 @@ async function create () { // MOUNTED -onMounted(loadGroups) +onMounted(() => { + state.userSendWelcomeEmailFromSiteId = adminStore.currentSiteId + loadGroups() +})