Procházet zdrojové kódy

feat: user welcome email

NGPixel před 1 rokem
rodič
revize
b7d25473c6

+ 1 - 0
server/graph/schemas/user.graphql

@@ -36,6 +36,7 @@ extend type Mutation {
     groups: [UUID]!
     mustChangePassword: Boolean!
     sendWelcomeEmail: Boolean!
+    sendWelcomeEmailFromSiteId: UUID
   ): UserResponse
 
   updateUser(

+ 2 - 0
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)",

+ 36 - 18
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.`)

+ 109 - 0
server/templates/mail/UserWelcome.vue

@@ -0,0 +1,109 @@
+<template>
+  <EHtml lang="en">
+    <EHead />
+    <EPreview>{{ preview }}</EPreview>
+    <EBody :style="main">
+      <EContainer :style="container">
+        <ESection :style="box">
+          <img :src="logo" height="50" :alt="siteTitle" />
+          <EHr :style="hr" />
+          <EText :style="paragraph"> {{ title }} </EText>
+          <EText :style="paragraph"> <b>Email Address:</b> {{ email }} </EText>
+          <EText :style="paragraph"> <b>Password:</b> {{ password }} </EText>
+          <EText :style="paragraph"> {{ content }} </EText>
+          <EButton px="10" py="10" :style="button" :href="buttonLink"> {{ buttonText }} </EButton>
+          <EHr :style="hr" />
+          <EText :style="footer"> <b>{{ siteTitle }}</b> </EText>
+          <EText :style="footer"> Wiki.js, an open source project. </EText>
+        </ESection>
+      </EContainer>
+    </EBody>
+  </EHtml>
+</template>
+
+<script setup>
+const props = defineProps({
+  preview: {
+    type: String,
+    default: '',
+  },
+  siteTitle: {
+    type: String,
+    default: ''
+  },
+  title: {
+    type: String,
+    default: '',
+  },
+  content: {
+    type: String,
+    default: '',
+  },
+  email: {
+    type: String,
+    default: ''
+  },
+  password: {
+    type: String,
+    default: ''
+  },
+  buttonLink: {
+    type: String,
+    default: '',
+  },
+  buttonText: {
+    type: String,
+    default: '',
+  },
+  logo: {
+    type: String,
+    default: ''
+  }
+})
+
+const main = {
+  backgroundColor: '#f6f9fc',
+  fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
+}
+
+const container = {
+  backgroundColor: '#ffffff',
+  margin: '0 auto',
+  padding: '20px 0 48px',
+  marginBottom: '64px',
+}
+
+const box = {
+  padding: '0 48px',
+}
+
+const hr = {
+  borderColor: '#e6ebf1',
+  margin: '20px 0',
+}
+
+const paragraph = {
+  color: '#525f7f',
+  fontSize: '16px',
+  lineHeight: '24px',
+  textAlign: 'left',
+}
+
+const button = {
+  backgroundColor: '#656ee8',
+  borderRadius: '5px',
+  color: '#fff',
+  fontSize: '16px',
+  fontWeight: 'bold',
+  textDecoration: 'none',
+  textAlign: 'center',
+  display: 'block',
+  width: '100%',
+}
+
+const footer = {
+  color: '#8898aa',
+  fontSize: '12px',
+  lineHeight: '16px',
+}
+</script>

+ 1 - 0
ux/public/_assets/icons/ultraviolet-web-design.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 40 40" width="80px" height="80px"><path fill="#fff" d="M2.5 3.5H37.5V36.5H2.5z"/><path fill="#4788c7" d="M37,4v32H3V4H37 M38,3H2v34h36V3L38,3z"/><path fill="#98ccfd" d="M3 4H37V12H3z"/><path fill="#fff" d="M6 7H8V9H6zM10 7H12V9H10zM14 7H16V9H14z"/><path fill="#98ccfd" d="M6 16H19V33H6z"/><path fill="#4788c7" d="M33.5 17h-11c-.275 0-.5-.225-.5-.5l0 0c0-.275.225-.5.5-.5h11c.275 0 .5.225.5.5l0 0C34 16.775 33.775 17 33.5 17zM33.5 21h-11c-.275 0-.5-.225-.5-.5l0 0c0-.275.225-.5.5-.5h11c.275 0 .5.225.5.5l0 0C34 20.775 33.775 21 33.5 21zM33.5 25h-11c-.275 0-.5-.225-.5-.5l0 0c0-.275.225-.5.5-.5h11c.275 0 .5.225.5.5l0 0C34 24.775 33.775 25 33.5 25zM33.5 29h-11c-.275 0-.5-.225-.5-.5l0 0c0-.275.225-.5.5-.5h11c.275 0 .5.225.5.5l0 0C34 28.775 33.775 29 33.5 29zM33.5 33h-11c-.275 0-.5-.225-.5-.5l0 0c0-.275.225-.5.5-.5h11c.275 0 .5.225.5.5l0 0C34 32.775 33.775 33 33.5 33z"/></svg>

+ 36 - 2
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()
+})
 </script>