From 4512b6711aab75d7a37c8bc7070d5d997975fb1e Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 29 Nov 2018 16:23:45 +0900 Subject: [PATCH] Implement email config --- locales/ja-JP.yml | 13 +++ package.json | 2 + src/client/app/admin/views/instance.vue | 48 +++++++++-- .../views/components/profile-editor.vue | 24 +++++- src/models/meta.ts | 8 ++ src/models/user.ts | 8 +- src/server/api/endpoints/admin/update-meta.ts | 79 ++++++++++++++++- src/server/api/endpoints/i/update_email.ts | 85 +++++++++++++++++++ src/server/api/endpoints/meta.ts | 7 ++ src/server/index.ts | 19 +++++ 10 files changed, 280 insertions(+), 13 deletions(-) create mode 100644 src/server/api/endpoints/i/update_email.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 239533998..23a2f64d0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -503,6 +503,10 @@ common/views/components/profile-editor.vue: saved: "プロフィールを保存しました" uploading: "アップロード中" upload-failed: "アップロードに失敗しました" + email: "メール設定" + email-address: "メールアドレス" + email-verified: "メールアドレスが確認されました" + email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" common/views/widgets/broadcast.vue: fetching: "確認中" @@ -1123,6 +1127,15 @@ admin/views/instance.vue: external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" external-user-recommendation-timeout: "タイムアウト" external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)" + email-config: "メールサーバーの設定" + email-config-info: "メールアドレス確認やパスワードリセットの際に使われます。" + enable-email: "メール配信を有効にする" + email: "メールアドレス" + smtp-use-ssl: "SMTPサーバーはSSLを使用" + smtp-host: "SMTPホスト" + smtp-port: "SMTPポート" + smtp-user: "SMTPユーザー" + smtp-pass: "SMTPパスワード" admin/views/charts.vue: title: "チャート" diff --git a/package.json b/package.json index f42000ab7..608cf7634 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@types/mongodb": "3.1.14", "@types/ms": "0.7.30", "@types/node": "10.12.10", + "@types/nodemailer": "4.6.5", "@types/oauth": "0.9.1", "@types/parsimmon": "1.10.0", "@types/portscanner": "2.1.0", @@ -166,6 +167,7 @@ "ms": "2.1.1", "nan": "2.11.1", "nested-property": "0.0.7", + "nodemailer": "4.7.0", "nprogress": "0.2.0", "object-assign-deep": "0.4.0", "on-build-webpack": "0.1.0", diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index da27b1e47..8609abf1e 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -12,11 +12,15 @@
{{ $t('maintainer-config') }}
{{ $t('maintainer-name') }} - {{ $t('maintainer-email') }} + {{ $t('maintainer-email') }}
{{ $t('max-note-text-length') }}
+
+ {{ $t('disable-registration') }} + {{ $t('disable-local-timeline') }} +
{{ $t('drive-config') }}
{{ $t('cache-remote-files') }}{{ $t('cache-remote-files-desc') }} @@ -37,10 +41,18 @@ {{ $t('proxy-account-warn') }}
- {{ $t('disable-registration') }} -
-
- {{ $t('disable-local-timeline') }} +
{{ $t('email-config') }}
+ {{ $t('enable-email') }}{{ $t('email-config-info') }} + {{ $t('email') }} + + {{ $t('smtp-host') }} + {{ $t('smtp-port') }} + + + {{ $t('smtp-user') }} + {{ $t('smtp-pass') }} + + {{ $t('smtp-use-ssl') }}
summaly Proxy
@@ -106,6 +118,7 @@ import i18n from '../../i18n'; import { url, host } from '../../config'; import { toUnicode } from 'punycode'; import { faHeadset, faShieldAlt, faGhost, faUserPlus } from '@fortawesome/free-solid-svg-icons'; +import { faEnvelope as farEnvelope } from '@fortawesome/free-regular-svg-icons'; export default Vue.extend({ i18n: i18n('admin/views/instance.vue'), @@ -144,7 +157,14 @@ export default Vue.extend({ externalUserRecommendationEngine: null, externalUserRecommendationTimeout: null, summalyProxy: null, - faHeadset, faShieldAlt, faGhost, faUserPlus + enableEmail: false, + email: null, + smtpSecure: false, + smtpHost: null, + smtpPort: null, + smtpUser: null, + smtpPass: null, + faHeadset, faShieldAlt, faGhost, faUserPlus, farEnvelope }; }, @@ -177,6 +197,13 @@ export default Vue.extend({ this.externalUserRecommendationEngine = meta.externalUserRecommendationEngine; this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout; this.summalyProxy = meta.summalyProxy; + this.enableEmail = meta.enableEmail; + this.email = meta.email; + this.smtpSecure = meta.smtpSecure; + this.smtpHost = meta.smtpHost; + this.smtpPort = meta.smtpPort; + this.smtpUser = meta.smtpUser; + this.smtpPass = meta.smtpPass; }); }, @@ -222,7 +249,14 @@ export default Vue.extend({ enableExternalUserRecommendation: this.enableExternalUserRecommendation, externalUserRecommendationEngine: this.externalUserRecommendationEngine, externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10), - summalyProxy: this.summalyProxy + summalyProxy: this.summalyProxy, + enableEmail: this.enableEmail, + email: this.email, + smtpSecure: this.smtpSecure, + smtpHost: this.smtpHost, + smtpPort: parseInt(this.smtpPort, 10), + smtpUser: this.smtpUser, + smtpPass: this.smtpPass }).then(() => { this.$root.alert({ type: 'success', diff --git a/src/client/app/common/views/components/profile-editor.vue b/src/client/app/common/views/components/profile-editor.vue index c3118126d..fc0fbb9e6 100644 --- a/src/client/app/common/views/components/profile-editor.vue +++ b/src/client/app/common/views/components/profile-editor.vue @@ -66,6 +66,19 @@ {{ $t('careful-bot') }}
+ +
+
{{ $t('email') }}
+ +
+ + {{ $t('email-address') }} + {{ $t('save') }} +
+
@@ -77,9 +90,11 @@ import { toUnicode } from 'punycode'; export default Vue.extend({ i18n: i18n('common/views/components/profile-editor.vue'), + data() { return { host: toUnicode(host), + email: null, name: null, username: null, location: null, @@ -113,7 +128,8 @@ export default Vue.extend({ }, created() { - this.name = this.$store.state.i.name || ''; + this.email = this.$store.state.i.email; + this.name = this.$store.state.i.name; this.username = this.$store.state.i.username; this.location = this.$store.state.i.profile.location; this.description = this.$store.state.i.description; @@ -199,6 +215,12 @@ export default Vue.extend({ }); } }); + }, + + updateEmail() { + this.$root.api('i/update_email', { + email: this.email == '' ? null : this.email + }); } } }); diff --git a/src/models/meta.ts b/src/models/meta.ts index 99d770366..c8ef18a69 100644 --- a/src/models/meta.ts +++ b/src/models/meta.ts @@ -214,4 +214,12 @@ export type IMeta = { enableExternalUserRecommendation?: boolean; externalUserRecommendationEngine?: string; externalUserRecommendationTimeout?: number; + + enableEmail?: boolean; + email?: string; + smtpSecure?: boolean; + smtpHost?: string; + smtpPort?: number; + smtpUser?: string; + smtpPass?: string; }; diff --git a/src/models/user.ts b/src/models/user.ts index 241af892a..db10e06d8 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -78,6 +78,8 @@ export interface ILocalUser extends IUserBase { host: null; keypair: string; email: string; + emailVerified?: boolean; + emailVerifyCode?: string; password: string; token: string; twitter: { @@ -99,9 +101,6 @@ export interface ILocalUser extends IUserBase { username: string; discriminator: string; }; - line: { - userId: string; - }; profile: { location: string; birthday: string; // 'YYYY-MM-DD' @@ -286,6 +285,7 @@ export const pack = ( delete _user._id; delete _user.usernameLower; + delete _user.emailVerifyCode; if (_user.host == null) { // Remove private properties @@ -306,11 +306,11 @@ export const pack = ( delete _user.discord.refreshToken; delete _user.discord.expiresDate; } - delete _user.line; // Visible via only the official client if (!opts.includeSecrets) { delete _user.email; + delete _user.emailVerified; delete _user.settings; delete _user.clientSettings; } diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index cff9ff8e5..3f3cd4a84 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -228,7 +228,56 @@ export const meta = { desc: { 'ja-JP': '外部ユーザーレコメンデーションのタイムアウト (ミリ秒)' } - } + }, + + enableEmail: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'メール配信を有効にするか否か' + } + }, + + email: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'メール配信する際に利用するメールアドレス' + } + }, + + smtpSecure: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'SMTPサーバがSSLを使用しているか否か' + } + }, + + smtpHost: { + validator: $.str.optional, + desc: { + 'ja-JP': 'SMTPサーバのホスト' + } + }, + + smtpPort: { + validator: $.num.optional, + desc: { + 'ja-JP': 'SMTPサーバのポート' + } + }, + + smtpUser: { + validator: $.str.optional, + desc: { + 'ja-JP': 'SMTPサーバのユーザー名' + } + }, + + smtpPass: { + validator: $.str.optional, + desc: { + 'ja-JP': 'SMTPサーバのパスワード' + } + }, } }; @@ -359,6 +408,34 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { set.externalUserRecommendationTimeout = ps.externalUserRecommendationTimeout; } + if (ps.enableEmail !== undefined) { + set.enableEmail = ps.enableEmail; + } + + if (ps.email !== undefined) { + set.email = ps.email; + } + + if (ps.smtpSecure !== undefined) { + set.smtpSecure = ps.smtpSecure; + } + + if (ps.smtpHost !== undefined) { + set.smtpHost = ps.smtpHost; + } + + if (ps.smtpPort !== undefined) { + set.smtpPort = ps.smtpPort; + } + + if (ps.smtpUser !== undefined) { + set.smtpUser = ps.smtpUser; + } + + if (ps.smtpPass !== undefined) { + set.smtpPass = ps.smtpPass; + } + await Meta.update({}, { $set: set }, { upsert: true }); diff --git a/src/server/api/endpoints/i/update_email.ts b/src/server/api/endpoints/i/update_email.ts new file mode 100644 index 000000000..c2699d47c --- /dev/null +++ b/src/server/api/endpoints/i/update_email.ts @@ -0,0 +1,85 @@ +import $ from 'cafy'; +import User, { pack } from '../../../../models/user'; +import { publishMainStream } from '../../../../stream'; +import define from '../../define'; +import * as nodemailer from 'nodemailer'; +import fetchMeta from '../../../../misc/fetch-meta'; +import rndstr from 'rndstr'; +import config from '../../../../config'; +const ms = require('ms'); + +export const meta = { + requireCredential: true, + + secure: true, + + limit: { + duration: ms('1hour'), + max: 3 + }, + + params: { + email: { + validator: $.str.optional.nullable + }, + } +}; + +export default define(meta, (ps, user) => new Promise(async (res, rej) => { + await User.update(user._id, { + $set: { + email: ps.email, + emailVerified: false, + emailVerifyCode: null + } + }); + + // Serialize + const iObj = await pack(user._id, user, { + detail: true, + includeSecrets: true + }); + + // Send response + res(iObj); + + // Publish meUpdated event + publishMainStream(user._id, 'meUpdated', iObj); + + if (ps.email != null) { + const code = rndstr('a-z0-9', 16); + + await User.update(user._id, { + $set: { + emailVerifyCode: code + } + }); + + const meta = await fetchMeta(); + + const transporter = nodemailer.createTransport({ + host: meta.smtpHost, + port: meta.smtpPort, + secure: meta.smtpSecure, + auth: { + user: meta.smtpUser, + pass: meta.smtpPass + } + }); + + const link = `${config.url}/vefify-email/${code}`; + + transporter.sendMail({ + from: meta.email, + to: ps.email, + subject: meta.name, + text: `To verify email, please click this link: ${link}` + }, (error, info) => { + if (error) { + return console.error(error); + } + + console.log('Message sent: %s', info.messageId); + }); + } +})); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 49ce41c7d..d18e6a154 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -108,6 +108,13 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { response.discordClientId = instance.discordClientId; response.discordClientSecret = instance.discordClientSecret; response.summalyProxy = instance.summalyProxy; + response.enableEmail = instance.enableEmail; + response.email = instance.email; + response.smtpSecure = instance.smtpSecure; + response.smtpHost = instance.smtpHost; + response.smtpPort = instance.smtpPort; + response.smtpUser = instance.smtpUser; + response.smtpPass = instance.smtpPass; } res(response); diff --git a/src/server/index.ts b/src/server/index.ts index e26f73ff4..88a39cd24 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -20,6 +20,7 @@ import config from '../config'; import networkChart from '../chart/network'; import apiServer from './api'; import { sum } from '../prelude/array'; +import User from '../models/user'; // Init app const app = new Koa(); @@ -59,6 +60,24 @@ const router = new Router(); router.use(activityPub.routes()); router.use(webFinger.routes()); +router.get('/verify-email/:code', async ctx => { + const user = await User.findOne({ emailVerifyCode: ctx.params.code }); + + if (user != null) { + ctx.body = 'Verify succeeded!'; + ctx.status = 200; + + User.update({ _id: user._id }, { + $set: { + emailVerified: true, + emailVerifyCode: null + } + }); + } else { + ctx.status = 404; + } +}); + // Return 404 for other .well-known router.all('/.well-known/*', async ctx => { ctx.status = 404;