From ffaec0b9712df9a5024c0883a154442f02b72a03 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 28 Aug 2017 23:47:43 +0900
Subject: [PATCH] #497

---
 CHANGELOG.md                                  |  8 +++-
 locales/en.yml                                |  4 ++
 locales/ja.yml                                |  4 ++
 src/api/common/generate-native-user-token.ts  |  3 ++
 src/api/endpoints.ts                          |  4 ++
 src/api/endpoints/i/regenerate_token.ts       | 42 +++++++++++++++++++
 src/api/private/signup.ts                     |  4 +-
 src/web/app/common/scripts/home-stream.js     |  6 +++
 src/web/app/common/tags/api-info.tag          | 27 ------------
 src/web/app/common/tags/index.js              |  1 -
 .../app/desktop/scripts/password-dialog.js    | 11 +++++
 src/web/app/desktop/tags/input-dialog.tag     |  7 +++-
 src/web/app/desktop/tags/settings.tag         | 30 +++++++++++++
 src/web/app/mobile/tags/page/settings/api.tag | 19 +++++++++
 14 files changed, 137 insertions(+), 33 deletions(-)
 create mode 100644 src/api/common/generate-native-user-token.ts
 create mode 100644 src/api/endpoints/i/regenerate_token.ts
 delete mode 100644 src/web/app/common/tags/api-info.tag
 create mode 100644 src/web/app/desktop/scripts/password-dialog.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bdbfdbe733..2d18b1b7f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog
 =========
 主に notable な changes を書いていきます
 
+unlereased
+----------
+* New: トークンを再生成できるように (#497)
+
 2461 (2017/08/28)
 -----------------
 * Fix: モバイル版からアバターとバナーの設定を行えなかった問題を修正
@@ -11,8 +15,8 @@ ChangeLog
 -----------------
 * New: モバイル版からプロフィールを設定できるように
 * New: モバイル版からサインアウトを行えるように
-* Improve: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
-* Improve: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
+* New: 投稿ページに次の投稿/前の投稿リンクを作成 (#734)
+* New: タイムラインの投稿をダブルクリックすることで詳細な情報が見れるように
 * Fix: モバイル版でおすすめユーザーをフォローしてもタイムラインが更新されない (#736)
 * Fix: モバイル版で設定にアクセスできない
 * デザインの調整
diff --git a/locales/en.yml b/locales/en.yml
index bfec7ebb54..950180278d 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -28,6 +28,7 @@ common:
   loading: "Loading"
   ok: "OK"
   update-available: "New version of Misskey is now available({newer}, current is {current}). Reload page to apply update."
+  my-token-regenerated: "Your token is just regenerated, so you will signout."
 
   tags:
     mk-messaging-form:
@@ -129,6 +130,9 @@ common:
 
 desktop:
   tags:
+    mk-api-info:
+      regenerate-token: "Please enter the password"
+
     mk-drive-browser-base-contextmenu:
       create-folder: "Create a folder"
       upload: "Upload a file"
diff --git a/locales/ja.yml b/locales/ja.yml
index e2c537fc41..2655eb4846 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -28,6 +28,7 @@ common:
   loading: "読み込み中"
   ok: "わかった"
   update-available: "Misskeyの新しいバージョンがあります({newer}。現在{current}を利用中)。ページを再度読み込みすると更新が適用されます。"
+  my-token-regenerated: "あなたのトークンが更新されたのでサインアウトします。"
 
   tags:
     mk-messaging-form:
@@ -129,6 +130,9 @@ common:
 
 desktop:
   tags:
+    mk-api-info:
+      regenerate-token: "パスワードを入力してください"
+
     mk-drive-browser-base-contextmenu:
       create-folder: "フォルダーを作成"
       upload: "ファイルをアップロード"
diff --git a/src/api/common/generate-native-user-token.ts b/src/api/common/generate-native-user-token.ts
new file mode 100644
index 0000000000..2082b89a5a
--- /dev/null
+++ b/src/api/common/generate-native-user-token.ts
@@ -0,0 +1,3 @@
+import rndstr from 'rndstr';
+
+export default () => `!${rndstr('a-zA-Z0-9', 32)}`;
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 5bbc480a8e..a658c9a42e 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -159,6 +159,10 @@ const endpoints: Endpoint[] = [
 		},
 		kind: 'account-write'
 	},
+	{
+		name: 'i/regenerate_token',
+		withCredential: true
+	},
 	{
 		name: 'i/appdata/get',
 		withCredential: true
diff --git a/src/api/endpoints/i/regenerate_token.ts b/src/api/endpoints/i/regenerate_token.ts
new file mode 100644
index 0000000000..ccebbc8101
--- /dev/null
+++ b/src/api/endpoints/i/regenerate_token.ts
@@ -0,0 +1,42 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import User from '../../models/user';
+import event from '../../event';
+import generateUserToken from '../../common/generate-native-user-token';
+
+/**
+ * Regenerate native token
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+	// Get 'password' parameter
+	const [password, passwordErr] = $(params.password).string().$;
+	if (passwordErr) return rej('invalid password param');
+
+	// Compare password
+	const same = bcrypt.compareSync(password, user.password);
+
+	if (!same) {
+		return rej('incorrect password');
+	}
+
+	// Generate secret
+	const secret = generateUserToken();
+
+	await User.update(user._id, {
+		$set: {
+			token: secret
+		}
+	});
+
+	res();
+
+	// Publish i updated event
+	event(user._id, 'my_token_regenerated');
+});
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 2375c22845..899fa88472 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -1,10 +1,10 @@
 import * as express from 'express';
 import * as bcrypt from 'bcryptjs';
-import rndstr from 'rndstr';
 import recaptcha = require('recaptcha-promise');
 import User from '../models/user';
 import { validateUsername, validatePassword } from '../models/user';
 import serialize from '../serializers/user';
+import generateUserToken from '../common/generate-native-user-token';
 import config from '../../conf';
 
 recaptcha.init({
@@ -58,7 +58,7 @@ export default async (req: express.Request, res: express.Response) => {
 	const hash = bcrypt.hashSync(password, salt);
 
 	// Generate secret
-	const secret = `!${rndstr('a-zA-Z0-9', 32)}`;
+	const secret = generateUserToken();
 
 	// Create account
 	const account = await User.insert({
diff --git a/src/web/app/common/scripts/home-stream.js b/src/web/app/common/scripts/home-stream.js
index 24f13cd291..c54cbd7f19 100644
--- a/src/web/app/common/scripts/home-stream.js
+++ b/src/web/app/common/scripts/home-stream.js
@@ -1,6 +1,7 @@
 'use strict';
 
 import Stream from './stream';
+import signout from './signout';
 
 /**
  * Home stream connection
@@ -12,6 +13,11 @@ class Connection extends Stream {
 		});
 
 		this.on('i_updated', me.update);
+
+		this.on('my_token_regenerated', () => {
+			alert('%i18n:common.my-token-regenerated%');
+			signout();
+		});
 	}
 }
 
diff --git a/src/web/app/common/tags/api-info.tag b/src/web/app/common/tags/api-info.tag
deleted file mode 100644
index 612f20a7a8..0000000000
--- a/src/web/app/common/tags/api-info.tag
+++ /dev/null
@@ -1,27 +0,0 @@
-<mk-api-info>
-	<p>Token:<code>{ I.token }</code></p>
-	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
-	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
-	<p>万が一このトークンが漏れたりその可能性がある場合は
-		<button class="regenerate" onclick={ regenerateToken }>トークンを再生成</button>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)
-	</p>
-	<style>
-		:scope
-			display block
-			color #4a535a
-
-			code
-				padding 4px
-				background #eee
-
-			.regenerate
-				display inline
-				color $theme-color
-
-				&:hover
-					text-decoration underline
-	</style>
-	<script>
-		this.mixin('i');
-	</script>
-</mk-api-info>
diff --git a/src/web/app/common/tags/index.js b/src/web/app/common/tags/index.js
index 5dc4ef4546..1ee8dab42d 100644
--- a/src/web/app/common/tags/index.js
+++ b/src/web/app/common/tags/index.js
@@ -14,7 +14,6 @@ require('./forkit.tag');
 require('./introduction.tag');
 require('./copyright.tag');
 require('./signin-history.tag');
-require('./api-info.tag');
 require('./twitter-setting.tag');
 require('./authorized-apps.tag');
 require('./poll.tag');
diff --git a/src/web/app/desktop/scripts/password-dialog.js b/src/web/app/desktop/scripts/password-dialog.js
new file mode 100644
index 0000000000..2bdc93e421
--- /dev/null
+++ b/src/web/app/desktop/scripts/password-dialog.js
@@ -0,0 +1,11 @@
+import * as riot from 'riot';
+
+export default (title, onOk, onCancel) => {
+	const dialog = document.body.appendChild(document.createElement('mk-input-dialog'));
+	return riot.mount(dialog, {
+		title: title,
+		type: 'password',
+		onOk: onOk,
+		onCancel: onCancel
+	});
+};
diff --git a/src/web/app/desktop/tags/input-dialog.tag b/src/web/app/desktop/tags/input-dialog.tag
index f343c4625a..78fd62ee8b 100644
--- a/src/web/app/desktop/tags/input-dialog.tag
+++ b/src/web/app/desktop/tags/input-dialog.tag
@@ -5,7 +5,7 @@
 		</yield>
 		<yield to="content">
 			<div class="body">
-				<input ref="text" oninput={ parent.update } onkeydown={ parent.onKeydown } placeholder={ parent.placeholder }/>
+				<input ref="text" type={ parent.type } oninput={ parent.onInput } onkeydown={ parent.onKeydown } placeholder={ parent.placeholder }/>
 			</div>
 			<div class="action">
 				<button class="cancel" onclick={ parent.cancel }>キャンセル</button>
@@ -126,6 +126,7 @@
 		this.placeholder = this.opts.placeholder;
 		this.default = this.opts.default;
 		this.allowEmpty = this.opts.allowEmpty != null ? this.opts.allowEmpty : true;
+		this.type = this.opts.type ? this.opts.type : 'text';
 
 		this.on('mount', () => {
 			this.text = this.refs.window.refs.text;
@@ -156,6 +157,10 @@
 			this.refs.window.close();
 		};
 
+		this.onInput = () => {
+			this.update();
+		};
+
 		this.onKeydown = e => {
 			if (e.which == 13) { // Enter
 				e.preventDefault();
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index a89cfda0e4..7fc6acb4a8 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -211,3 +211,33 @@
 		};
 	</script>
 </mk-settings>
+
+<mk-api-info>
+	<p>Token:<code>{ I.token }</code></p>
+	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
+	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
+	<p>万が一このトークンが漏れたりその可能性がある場合は<a class="regenerate" onclick={ regenerateToken }>トークンを再生成</a>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)</p>
+	<style>
+		:scope
+			display block
+			color #4a535a
+
+			code
+				padding 4px
+				background #eee
+	</style>
+	<script>
+		import passwordDialog from '../scripts/password-dialog';
+
+		this.mixin('i');
+		this.mixin('api');
+
+		this.regenerateToken = () => {
+			passwordDialog('%i18n:desktop.tags.mk-api-info.regenerate-token%', password => {
+				this.api('i/regenerate_token', {
+					password: password
+				})
+			});
+		};
+	</script>
+</mk-api-info>
diff --git a/src/web/app/mobile/tags/page/settings/api.tag b/src/web/app/mobile/tags/page/settings/api.tag
index 46419eb3db..25413e2d80 100644
--- a/src/web/app/mobile/tags/page/settings/api.tag
+++ b/src/web/app/mobile/tags/page/settings/api.tag
@@ -15,3 +15,22 @@
 		});
 	</script>
 </mk-api-info-page>
+
+<mk-api-info>
+	<p>Token:<code>{ I.token }</code></p>
+	<p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
+	<p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
+	<p>万が一このトークンが漏れたりその可能性がある場合はデスクトップ版Misskeyから再生成できます。</p>
+	<style>
+		:scope
+			display block
+			color #4a535a
+
+			code
+				padding 4px
+				background #eee
+	</style>
+	<script>
+		this.mixin('i');
+	</script>
+</mk-api-info>