Implement email config

This commit is contained in:
syuilo 2018-11-29 16:23:45 +09:00
parent 0ce64f8c33
commit 1bc109b42c
No known key found for this signature in database
GPG key ID: BDC4C49D06AB9D69
10 changed files with 280 additions and 13 deletions

View file

@ -503,6 +503,10 @@ common/views/components/profile-editor.vue:
saved: "プロフィールを保存しました" saved: "プロフィールを保存しました"
uploading: "アップロード中" uploading: "アップロード中"
upload-failed: "アップロードに失敗しました" upload-failed: "アップロードに失敗しました"
email: "メール設定"
email-address: "メールアドレス"
email-verified: "メールアドレスが確認されました"
email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。"
common/views/widgets/broadcast.vue: common/views/widgets/broadcast.vue:
fetching: "確認中" 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-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: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)" 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: admin/views/charts.vue:
title: "チャート" title: "チャート"

View file

@ -64,6 +64,7 @@
"@types/mongodb": "3.1.14", "@types/mongodb": "3.1.14",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.12.10", "@types/node": "10.12.10",
"@types/nodemailer": "4.6.5",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
"@types/parsimmon": "1.10.0", "@types/parsimmon": "1.10.0",
"@types/portscanner": "2.1.0", "@types/portscanner": "2.1.0",
@ -166,6 +167,7 @@
"ms": "2.1.1", "ms": "2.1.1",
"nan": "2.11.1", "nan": "2.11.1",
"nested-property": "0.0.7", "nested-property": "0.0.7",
"nodemailer": "4.7.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"object-assign-deep": "0.4.0", "object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0", "on-build-webpack": "0.1.0",

View file

@ -12,11 +12,15 @@
<section class="fit-bottom"> <section class="fit-bottom">
<header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header> <header><fa :icon="faHeadset"/> {{ $t('maintainer-config') }}</header>
<ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input> <ui-input v-model="maintainerName">{{ $t('maintainer-name') }}</ui-input>
<ui-input v-model="maintainerEmail" type="email"><i slot="icon"><fa :icon="['far', 'envelope']"/></i>{{ $t('maintainer-email') }}</ui-input> <ui-input v-model="maintainerEmail" type="email"><i slot="icon"><fa :icon="farEnvelope"/></i>{{ $t('maintainer-email') }}</ui-input>
</section> </section>
<section class="fit-top fit-bottom"> <section class="fit-top fit-bottom">
<ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input> <ui-input v-model="maxNoteTextLength">{{ $t('max-note-text-length') }}</ui-input>
</section> </section>
<section>
<ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch>
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
</section>
<section class="fit-bottom"> <section class="fit-bottom">
<header><fa icon="cloud"/> {{ $t('drive-config') }}</header> <header><fa icon="cloud"/> {{ $t('drive-config') }}</header>
<ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<span slot="desc">{{ $t('cache-remote-files-desc') }}</span></ui-switch> <ui-switch v-model="cacheRemoteFiles">{{ $t('cache-remote-files') }}<span slot="desc">{{ $t('cache-remote-files-desc') }}</span></ui-switch>
@ -37,10 +41,18 @@
<ui-info warn>{{ $t('proxy-account-warn') }}</ui-info> <ui-info warn>{{ $t('proxy-account-warn') }}</ui-info>
</section> </section>
<section> <section>
<ui-switch v-model="disableRegistration">{{ $t('disable-registration') }}</ui-switch> <header><fa :icon="farEnvelope"/> {{ $t('email-config') }}</header>
</section> <ui-switch v-model="enableEmail">{{ $t('enable-email') }}<span slot="desc">{{ $t('email-config-info') }}</span></ui-switch>
<section> <ui-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</ui-input>
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch> <ui-horizon-group inputs>
<ui-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtp-host') }}</ui-input>
<ui-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtp-port') }}</ui-input>
</ui-horizon-group>
<ui-horizon-group inputs>
<ui-input v-model="smtpUser" :disabled="!enableEmail">{{ $t('smtp-user') }}</ui-input>
<ui-input v-model="smtpPass" :disabled="!enableEmail">{{ $t('smtp-pass') }}</ui-input>
</ui-horizon-group>
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-use-ssl') }}</ui-switch>
</section> </section>
<section> <section>
<header>summaly Proxy</header> <header>summaly Proxy</header>
@ -106,6 +118,7 @@ import i18n from '../../i18n';
import { url, host } from '../../config'; import { url, host } from '../../config';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import { faHeadset, faShieldAlt, faGhost, faUserPlus } from '@fortawesome/free-solid-svg-icons'; 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({ export default Vue.extend({
i18n: i18n('admin/views/instance.vue'), i18n: i18n('admin/views/instance.vue'),
@ -144,7 +157,14 @@ export default Vue.extend({
externalUserRecommendationEngine: null, externalUserRecommendationEngine: null,
externalUserRecommendationTimeout: null, externalUserRecommendationTimeout: null,
summalyProxy: 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.externalUserRecommendationEngine = meta.externalUserRecommendationEngine;
this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout; this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout;
this.summalyProxy = meta.summalyProxy; 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, enableExternalUserRecommendation: this.enableExternalUserRecommendation,
externalUserRecommendationEngine: this.externalUserRecommendationEngine, externalUserRecommendationEngine: this.externalUserRecommendationEngine,
externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10), 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(() => { }).then(() => {
this.$root.alert({ this.$root.alert({
type: 'success', type: 'success',

View file

@ -66,6 +66,19 @@
<ui-switch v-model="carefulBot" @change="save(false)">{{ $t('careful-bot') }}</ui-switch> <ui-switch v-model="carefulBot" @change="save(false)">{{ $t('careful-bot') }}</ui-switch>
</div> </div>
</section> </section>
<section>
<header>{{ $t('email') }}</header>
<div>
<template v-if="$store.state.i.email != null">
<ui-info v-if="$store.state.i.emailVerified">{{ $t('email-verified') }}</ui-info>
<ui-info v-else warn>{{ $t('email-not-verified') }}</ui-info>
</template>
<ui-input v-model="email" type="email"><span>{{ $t('email-address') }}</span></ui-input>
<ui-button @click="updateEmail()">{{ $t('save') }}</ui-button>
</div>
</section>
</ui-card> </ui-card>
</template> </template>
@ -77,9 +90,11 @@ import { toUnicode } from 'punycode';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/profile-editor.vue'), i18n: i18n('common/views/components/profile-editor.vue'),
data() { data() {
return { return {
host: toUnicode(host), host: toUnicode(host),
email: null,
name: null, name: null,
username: null, username: null,
location: null, location: null,
@ -113,7 +128,8 @@ export default Vue.extend({
}, },
created() { 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.username = this.$store.state.i.username;
this.location = this.$store.state.i.profile.location; this.location = this.$store.state.i.profile.location;
this.description = this.$store.state.i.description; 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
});
} }
} }
}); });

View file

@ -214,4 +214,12 @@ export type IMeta = {
enableExternalUserRecommendation?: boolean; enableExternalUserRecommendation?: boolean;
externalUserRecommendationEngine?: string; externalUserRecommendationEngine?: string;
externalUserRecommendationTimeout?: number; externalUserRecommendationTimeout?: number;
enableEmail?: boolean;
email?: string;
smtpSecure?: boolean;
smtpHost?: string;
smtpPort?: number;
smtpUser?: string;
smtpPass?: string;
}; };

View file

@ -78,6 +78,8 @@ export interface ILocalUser extends IUserBase {
host: null; host: null;
keypair: string; keypair: string;
email: string; email: string;
emailVerified?: boolean;
emailVerifyCode?: string;
password: string; password: string;
token: string; token: string;
twitter: { twitter: {
@ -99,9 +101,6 @@ export interface ILocalUser extends IUserBase {
username: string; username: string;
discriminator: string; discriminator: string;
}; };
line: {
userId: string;
};
profile: { profile: {
location: string; location: string;
birthday: string; // 'YYYY-MM-DD' birthday: string; // 'YYYY-MM-DD'
@ -286,6 +285,7 @@ export const pack = (
delete _user._id; delete _user._id;
delete _user.usernameLower; delete _user.usernameLower;
delete _user.emailVerifyCode;
if (_user.host == null) { if (_user.host == null) {
// Remove private properties // Remove private properties
@ -306,11 +306,11 @@ export const pack = (
delete _user.discord.refreshToken; delete _user.discord.refreshToken;
delete _user.discord.expiresDate; delete _user.discord.expiresDate;
} }
delete _user.line;
// Visible via only the official client // Visible via only the official client
if (!opts.includeSecrets) { if (!opts.includeSecrets) {
delete _user.email; delete _user.email;
delete _user.emailVerified;
delete _user.settings; delete _user.settings;
delete _user.clientSettings; delete _user.clientSettings;
} }

View file

@ -228,7 +228,56 @@ export const meta = {
desc: { desc: {
'ja-JP': '外部ユーザーレコメンデーションのタイムアウト (ミリ秒)' '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; 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({}, { await Meta.update({}, {
$set: set $set: set
}, { upsert: true }); }, { upsert: true });

View file

@ -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);
});
}
}));

View file

@ -108,6 +108,13 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
response.discordClientId = instance.discordClientId; response.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret; response.discordClientSecret = instance.discordClientSecret;
response.summalyProxy = instance.summalyProxy; 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); res(response);

View file

@ -20,6 +20,7 @@ import config from '../config';
import networkChart from '../chart/network'; import networkChart from '../chart/network';
import apiServer from './api'; import apiServer from './api';
import { sum } from '../prelude/array'; import { sum } from '../prelude/array';
import User from '../models/user';
// Init app // Init app
const app = new Koa(); const app = new Koa();
@ -59,6 +60,24 @@ const router = new Router();
router.use(activityPub.routes()); router.use(activityPub.routes());
router.use(webFinger.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 // Return 404 for other .well-known
router.all('/.well-known/*', async ctx => { router.all('/.well-known/*', async ctx => {
ctx.status = 404; ctx.status = 404;