+
+
+
+
+ {{ $t('email-verified') }}
+ {{ $t('email-not-verified') }}
+
+ {{ $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;