This commit is contained in:
		
							parent
							
								
									0de62522a8
								
							
						
					
					
						commit
						6bc499f657
					
				
					 11 changed files with 221 additions and 4 deletions
				
			
		|  | @ -137,6 +137,7 @@ common: | ||||||
|     mk-signin: |     mk-signin: | ||||||
|       username: "Username" |       username: "Username" | ||||||
|       password: "Password" |       password: "Password" | ||||||
|  |       token: "Token" | ||||||
|       signing-in: "Signing in..." |       signing-in: "Signing in..." | ||||||
|       signin: "Sign in" |       signin: "Sign in" | ||||||
| 
 | 
 | ||||||
|  | @ -295,6 +296,17 @@ desktop: | ||||||
|       not-match: "New password not matched" |       not-match: "New password not matched" | ||||||
|       changed: "Password updated successfully" |       changed: "Password updated successfully" | ||||||
| 
 | 
 | ||||||
|  |     mk-2fa-setting: | ||||||
|  |       register: "Register a device" | ||||||
|  |       enter-password: "Enter the password" | ||||||
|  |       authenticator: "First, you need install Google Authenticator to your device:" | ||||||
|  |       howtoinstall: "How to install" | ||||||
|  |       scan: "Next, please scan displayed QR code:" | ||||||
|  |       done: "Please enter the token displaying in your device:" | ||||||
|  |       submit: "Submit" | ||||||
|  |       success: "Setup completed successfully!" | ||||||
|  |       failed: "Failed to setup. please ensure that the token is correct." | ||||||
|  | 
 | ||||||
|     mk-post-form: |     mk-post-form: | ||||||
|       post-placeholder: "What's happening?" |       post-placeholder: "What's happening?" | ||||||
|       reply-placeholder: "Reply to this post..." |       reply-placeholder: "Reply to this post..." | ||||||
|  | @ -327,7 +339,9 @@ desktop: | ||||||
|       next: "Next post" |       next: "Next post" | ||||||
| 
 | 
 | ||||||
|     mk-settings: |     mk-settings: | ||||||
|  |       security: "Security" | ||||||
|       password: "Password" |       password: "Password" | ||||||
|  |       2fa: "Two-factor authentication" | ||||||
| 
 | 
 | ||||||
|     mk-timeline-post: |     mk-timeline-post: | ||||||
|       reposted-by: "Reposted by {}" |       reposted-by: "Reposted by {}" | ||||||
|  |  | ||||||
|  | @ -137,6 +137,7 @@ common: | ||||||
|     mk-signin: |     mk-signin: | ||||||
|       username: "ユーザー名" |       username: "ユーザー名" | ||||||
|       password: "パスワード" |       password: "パスワード" | ||||||
|  |       token: "トークン" | ||||||
|       signing-in: "やってます..." |       signing-in: "やってます..." | ||||||
|       signin: "サインイン" |       signin: "サインイン" | ||||||
| 
 | 
 | ||||||
|  | @ -295,6 +296,17 @@ desktop: | ||||||
|       not-match: "新しいパスワードが一致しません" |       not-match: "新しいパスワードが一致しません" | ||||||
|       changed: "パスワードを変更しました" |       changed: "パスワードを変更しました" | ||||||
| 
 | 
 | ||||||
|  |     mk-2fa-setting: | ||||||
|  |       register: "デバイスを登録する" | ||||||
|  |       enter-password: "パスワードを入力してください" | ||||||
|  |       authenticator: "まず、Google Authenticatorをお使いのデバイスにインストールします:" | ||||||
|  |       howtoinstall: "インストール方法はこちら" | ||||||
|  |       scan: "次に、表示されているQRコードをスキャンします:" | ||||||
|  |       done: "お使いのデバイスに表示されているトークンを入力して完了します:" | ||||||
|  |       submit: "完了" | ||||||
|  |       success: "設定が完了しました!" | ||||||
|  |       failed: "設定に失敗しました。トークンに誤りがないかご確認ください。" | ||||||
|  | 
 | ||||||
|     mk-post-form: |     mk-post-form: | ||||||
|       post-placeholder: "いまどうしてる?" |       post-placeholder: "いまどうしてる?" | ||||||
|       reply-placeholder: "この投稿への返信..." |       reply-placeholder: "この投稿への返信..." | ||||||
|  | @ -327,7 +339,9 @@ desktop: | ||||||
|       next: "次の投稿" |       next: "次の投稿" | ||||||
| 
 | 
 | ||||||
|     mk-settings: |     mk-settings: | ||||||
|  |       security: "セキュリティ" | ||||||
|       password: "パスワード" |       password: "パスワード" | ||||||
|  |       2fa: "二段階認証" | ||||||
| 
 | 
 | ||||||
|     mk-timeline-post: |     mk-timeline-post: | ||||||
|       reposted-by: "{}がRepost" |       reposted-by: "{}がRepost" | ||||||
|  |  | ||||||
|  | @ -62,6 +62,7 @@ | ||||||
| 		"@types/node": "8.0.57", | 		"@types/node": "8.0.57", | ||||||
| 		"@types/page": "1.5.32", | 		"@types/page": "1.5.32", | ||||||
| 		"@types/proxy-addr": "2.0.0", | 		"@types/proxy-addr": "2.0.0", | ||||||
|  | 		"@types/qrcode": "^0.8.0", | ||||||
| 		"@types/ratelimiter": "2.1.28", | 		"@types/ratelimiter": "2.1.28", | ||||||
| 		"@types/redis": "2.8.1", | 		"@types/redis": "2.8.1", | ||||||
| 		"@types/request": "2.0.8", | 		"@types/request": "2.0.8", | ||||||
|  | @ -69,6 +70,7 @@ | ||||||
| 		"@types/riot": "3.6.1", | 		"@types/riot": "3.6.1", | ||||||
| 		"@types/seedrandom": "2.4.27", | 		"@types/seedrandom": "2.4.27", | ||||||
| 		"@types/serve-favicon": "2.2.30", | 		"@types/serve-favicon": "2.2.30", | ||||||
|  | 		"@types/speakeasy": "^2.0.1", | ||||||
| 		"@types/tmp": "0.0.33", | 		"@types/tmp": "0.0.33", | ||||||
| 		"@types/uuid": "3.4.3", | 		"@types/uuid": "3.4.3", | ||||||
| 		"@types/webpack": "3.8.1", | 		"@types/webpack": "3.8.1", | ||||||
|  | @ -134,6 +136,7 @@ | ||||||
| 		"prominence": "0.2.0", | 		"prominence": "0.2.0", | ||||||
| 		"proxy-addr": "2.0.2", | 		"proxy-addr": "2.0.2", | ||||||
| 		"pug": "2.0.0-rc.4", | 		"pug": "2.0.0-rc.4", | ||||||
|  | 		"qrcode": "^1.0.0", | ||||||
| 		"ratelimiter": "3.0.3", | 		"ratelimiter": "3.0.3", | ||||||
| 		"recaptcha-promise": "0.1.3", | 		"recaptcha-promise": "0.1.3", | ||||||
| 		"reconnecting-websocket": "3.2.2", | 		"reconnecting-websocket": "3.2.2", | ||||||
|  | @ -147,6 +150,7 @@ | ||||||
| 		"seedrandom": "^2.4.3", | 		"seedrandom": "^2.4.3", | ||||||
| 		"serve-favicon": "2.4.5", | 		"serve-favicon": "2.4.5", | ||||||
| 		"sortablejs": "1.7.0", | 		"sortablejs": "1.7.0", | ||||||
|  | 		"speakeasy": "^2.0.0", | ||||||
| 		"string-replace-webpack-plugin": "0.1.3", | 		"string-replace-webpack-plugin": "0.1.3", | ||||||
| 		"style-loader": "0.19.0", | 		"style-loader": "0.19.0", | ||||||
| 		"stylus": "0.54.5", | 		"stylus": "0.54.5", | ||||||
|  |  | ||||||
|  | @ -155,6 +155,14 @@ const endpoints: Endpoint[] = [ | ||||||
| 		name: 'i', | 		name: 'i', | ||||||
| 		withCredential: true | 		withCredential: true | ||||||
| 	}, | 	}, | ||||||
|  | 	{ | ||||||
|  | 		name: 'i/2fa/register', | ||||||
|  | 		withCredential: true | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		name: 'i/2fa/done', | ||||||
|  | 		withCredential: true | ||||||
|  | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		name: 'i/update', | 		name: 'i/update', | ||||||
| 		withCredential: true, | 		withCredential: true, | ||||||
|  |  | ||||||
							
								
								
									
										37
									
								
								src/api/endpoints/i/2fa/done.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/api/endpoints/i/2fa/done.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | /** | ||||||
|  |  * Module dependencies | ||||||
|  |  */ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import * as speakeasy from 'speakeasy'; | ||||||
|  | import User from '../../../models/user'; | ||||||
|  | 
 | ||||||
|  | module.exports = async (params, user) => new Promise(async (res, rej) => { | ||||||
|  | 	// Get 'token' parameter
 | ||||||
|  | 	const [token, tokenErr] = $(params.token).string().$; | ||||||
|  | 	if (tokenErr) return rej('invalid token param'); | ||||||
|  | 
 | ||||||
|  | 	const _token = token.replace(/\s/g, ''); | ||||||
|  | 
 | ||||||
|  | 	if (user.two_factor_temp_secret == null) { | ||||||
|  | 		return rej('二段階認証の設定が開始されていません'); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const verified = (speakeasy as any).totp.verify({ | ||||||
|  | 		secret: user.two_factor_temp_secret, | ||||||
|  | 		encoding: 'base32', | ||||||
|  | 		token: _token | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (!verified) { | ||||||
|  | 		return rej('not verified'); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	await User.update(user._id, { | ||||||
|  | 		$set: { | ||||||
|  | 			two_factor_secret: user.two_factor_temp_secret, | ||||||
|  | 			two_factor_enabled: true | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	res(); | ||||||
|  | }); | ||||||
							
								
								
									
										48
									
								
								src/api/endpoints/i/2fa/register.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/api/endpoints/i/2fa/register.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | /** | ||||||
|  |  * Module dependencies | ||||||
|  |  */ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import * as bcrypt from 'bcryptjs'; | ||||||
|  | import * as speakeasy from 'speakeasy'; | ||||||
|  | import * as QRCode from 'qrcode'; | ||||||
|  | import User from '../../../models/user'; | ||||||
|  | import config from '../../../../conf'; | ||||||
|  | 
 | ||||||
|  | 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 = await bcrypt.compare(password, user.password); | ||||||
|  | 
 | ||||||
|  | 	if (!same) { | ||||||
|  | 		return rej('incorrect password'); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Generate user's secret key
 | ||||||
|  | 	const secret = speakeasy.generateSecret({ | ||||||
|  | 		length: 32 | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	await User.update(user._id, { | ||||||
|  | 		$set: { | ||||||
|  | 			two_factor_temp_secret: secret.base32 | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	// Get the data URL of the authenticator URL
 | ||||||
|  | 	QRCode.toDataURL(speakeasy.otpauthURL({ | ||||||
|  | 		secret: secret.base32, | ||||||
|  | 		encoding: 'base32', | ||||||
|  | 		label: user.username, | ||||||
|  | 		issuer: config.host | ||||||
|  | 	}), (err, data_url) => { | ||||||
|  | 		res({ | ||||||
|  | 			qr: data_url, | ||||||
|  | 			secret: secret.base32, | ||||||
|  | 			label: user.username, | ||||||
|  | 			issuer: config.host | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | @ -72,6 +72,8 @@ export type IUser = { | ||||||
| 	is_pro: boolean; | 	is_pro: boolean; | ||||||
| 	is_suspended: boolean; | 	is_suspended: boolean; | ||||||
| 	keywords: string[]; | 	keywords: string[]; | ||||||
|  | 	two_factor_secret: string; | ||||||
|  | 	two_factor_enabled: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export function init(user): IUser { | export function init(user): IUser { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import * as express from 'express'; | import * as express from 'express'; | ||||||
| import * as bcrypt from 'bcryptjs'; | import * as bcrypt from 'bcryptjs'; | ||||||
|  | import * as speakeasy from 'speakeasy'; | ||||||
| import { default as User, IUser } from '../models/user'; | import { default as User, IUser } from '../models/user'; | ||||||
| import Signin from '../models/signin'; | import Signin from '../models/signin'; | ||||||
| import serialize from '../serializers/signin'; | import serialize from '../serializers/signin'; | ||||||
|  | @ -11,6 +12,7 @@ export default async (req: express.Request, res: express.Response) => { | ||||||
| 
 | 
 | ||||||
| 	const username = req.body['username']; | 	const username = req.body['username']; | ||||||
| 	const password = req.body['password']; | 	const password = req.body['password']; | ||||||
|  | 	const token = req.body['token']; | ||||||
| 
 | 
 | ||||||
| 	if (typeof username != 'string') { | 	if (typeof username != 'string') { | ||||||
| 		res.sendStatus(400); | 		res.sendStatus(400); | ||||||
|  | @ -22,6 +24,11 @@ export default async (req: express.Request, res: express.Response) => { | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if (token != null && typeof token != 'string') { | ||||||
|  | 		res.sendStatus(400); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Fetch user
 | 	// Fetch user
 | ||||||
| 	const user: IUser = await User.findOne({ | 	const user: IUser = await User.findOne({ | ||||||
| 		username_lower: username.toLowerCase() | 		username_lower: username.toLowerCase() | ||||||
|  | @ -43,7 +50,23 @@ export default async (req: express.Request, res: express.Response) => { | ||||||
| 	const same = await bcrypt.compare(password, user.password); | 	const same = await bcrypt.compare(password, user.password); | ||||||
| 
 | 
 | ||||||
| 	if (same) { | 	if (same) { | ||||||
| 		signin(res, user, false); | 		if (user.two_factor_enabled) { | ||||||
|  | 			const verified = (speakeasy as any).totp.verify({ | ||||||
|  | 				secret: user.two_factor_secret, | ||||||
|  | 				encoding: 'base32', | ||||||
|  | 				token: token | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if (verified) { | ||||||
|  | 				signin(res, user, false); | ||||||
|  | 			} else { | ||||||
|  | 				res.status(400).send({ | ||||||
|  | 					error: 'invalid token' | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			signin(res, user, false); | ||||||
|  | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		res.status(400).send({ | 		res.status(400).send({ | ||||||
| 			error: 'incorrect password' | 			error: 'incorrect password' | ||||||
|  |  | ||||||
|  | @ -78,6 +78,8 @@ export default ( | ||||||
| 	// Remove private properties
 | 	// Remove private properties
 | ||||||
| 	delete _user.password; | 	delete _user.password; | ||||||
| 	delete _user.token; | 	delete _user.token; | ||||||
|  | 	delete _user.two_factor_temp_secret; | ||||||
|  | 	delete _user.two_factor_secret; | ||||||
| 	delete _user.username_lower; | 	delete _user.username_lower; | ||||||
| 	if (_user.twitter) { | 	if (_user.twitter) { | ||||||
| 		delete _user.twitter.access_token; | 		delete _user.twitter.access_token; | ||||||
|  | @ -91,6 +93,10 @@ export default ( | ||||||
| 		delete _user.client_settings; | 		delete _user.client_settings; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if (!opts.detail) { | ||||||
|  | 		delete _user.two_factor_enabled; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	_user.avatar_url = _user.avatar_id != null | 	_user.avatar_url = _user.avatar_id != null | ||||||
| 		? `${config.drive_url}/${_user.avatar_id}` | 		? `${config.drive_url}/${_user.avatar_id}` | ||||||
| 		: `${config.drive_url}/default-avatar.jpg`; | 		: `${config.drive_url}/default-avatar.jpg`; | ||||||
|  |  | ||||||
|  | @ -6,6 +6,9 @@ | ||||||
| 		<label class="password"> | 		<label class="password"> | ||||||
| 			<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/>%fa:lock% | 			<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/>%fa:lock% | ||||||
| 		</label> | 		</label> | ||||||
|  | 		<label class="token" if={ user && user.two_factor_enabled }> | ||||||
|  | 			<input ref="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required="required"/>%fa:lock% | ||||||
|  | 		</label> | ||||||
| 		<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button> | 		<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button> | ||||||
| 	</form> | 	</form> | ||||||
| 	<style> | 	<style> | ||||||
|  | @ -39,6 +42,7 @@ | ||||||
| 
 | 
 | ||||||
| 					input[type=text] | 					input[type=text] | ||||||
| 					input[type=password] | 					input[type=password] | ||||||
|  | 					input[type=number] | ||||||
| 						user-select text | 						user-select text | ||||||
| 						display inline-block | 						display inline-block | ||||||
| 						cursor auto | 						cursor auto | ||||||
|  | @ -123,6 +127,10 @@ | ||||||
| 				this.refs.password.focus(); | 				this.refs.password.focus(); | ||||||
| 				return false; | 				return false; | ||||||
| 			} | 			} | ||||||
|  | 			if (this.user && this.user.two_factor_enabled && this.refs.token.value == '') { | ||||||
|  | 				this.refs.token.focus(); | ||||||
|  | 				return false; | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
| 			this.update({ | 			this.update({ | ||||||
| 				signing: true | 				signing: true | ||||||
|  | @ -130,7 +138,8 @@ | ||||||
| 
 | 
 | ||||||
| 			this.api('signin', { | 			this.api('signin', { | ||||||
| 				username: this.refs.username.value, | 				username: this.refs.username.value, | ||||||
| 				password: this.refs.password.value | 				password: this.refs.password.value, | ||||||
|  | 				token: this.user && this.user.two_factor_enabled ? this.refs.token.value : undefined | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				location.reload(); | 				location.reload(); | ||||||
| 			}).catch(() => { | 			}).catch(() => { | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 		<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p> | 		<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p> | ||||||
| 		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p> | 		<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p> | ||||||
| 		<p class={ active: page == 'signin' } onmousedown={ setPage.bind(null, 'signin') }>%fa:sign-in-alt .fw%ログイン履歴</p> | 		<p class={ active: page == 'signin' } onmousedown={ setPage.bind(null, 'signin') }>%fa:sign-in-alt .fw%ログイン履歴</p> | ||||||
| 		<p class={ active: page == 'password' } onmousedown={ setPage.bind(null, 'password') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.password%</p> | 		<p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p> | ||||||
| 		<p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p> | 		<p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="pages"> | 	<div class="pages"> | ||||||
|  | @ -59,11 +59,16 @@ | ||||||
| 			<mk-signin-history/> | 			<mk-signin-history/> | ||||||
| 		</section> | 		</section> | ||||||
| 
 | 
 | ||||||
| 		<section class="password" show={ page == 'password' }> | 		<section class="password" show={ page == 'security' }> | ||||||
| 			<h1>%i18n:desktop.tags.mk-settings.password%</h1> | 			<h1>%i18n:desktop.tags.mk-settings.password%</h1> | ||||||
| 			<mk-password-setting/> | 			<mk-password-setting/> | ||||||
| 		</section> | 		</section> | ||||||
| 
 | 
 | ||||||
|  | 		<section class="2fa" show={ page == 'security' }> | ||||||
|  | 			<h1>%i18n:desktop.tags.mk-settings.2fa%</h1> | ||||||
|  | 			<mk-2fa-setting/> | ||||||
|  | 		</section> | ||||||
|  | 
 | ||||||
| 		<section class="api" show={ page == 'api' }> | 		<section class="api" show={ page == 'api' }> | ||||||
| 			<h1>API</h1> | 			<h1>API</h1> | ||||||
| 			<mk-api-info/> | 			<mk-api-info/> | ||||||
|  | @ -285,3 +290,50 @@ | ||||||
| 		}; | 		}; | ||||||
| 	</script> | 	</script> | ||||||
| </mk-password-setting> | </mk-password-setting> | ||||||
|  | 
 | ||||||
|  | <mk-2fa-setting> | ||||||
|  | 	<p><button onclick={ register }>%i18n:desktop.tags.mk-2fa-setting.register%</button></p> | ||||||
|  | 	<div if={ data }> | ||||||
|  | 		<ol> | ||||||
|  | 			<li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li> | ||||||
|  | 			<li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li> | ||||||
|  | 			<li>%i18n:desktop.tags.mk-2fa-setting.done%<br> | ||||||
|  | 				<input type="number" ref="token"><button onclick={ submit }>%i18n:desktop.tags.mk-2fa-setting.submit%</button> | ||||||
|  | 			</li> | ||||||
|  | 		</ol> | ||||||
|  | 	</div> | ||||||
|  | 	<style> | ||||||
|  | 		:scope | ||||||
|  | 			display block | ||||||
|  | 			color #4a535a | ||||||
|  | 
 | ||||||
|  | 	</style> | ||||||
|  | 	<script> | ||||||
|  | 		import passwordDialog from '../scripts/password-dialog'; | ||||||
|  | 		import notify from '../scripts/notify'; | ||||||
|  | 
 | ||||||
|  | 		this.mixin('api'); | ||||||
|  | 
 | ||||||
|  | 		this.register = () => { | ||||||
|  | 			passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => { | ||||||
|  | 				this.api('i/2fa/register', { | ||||||
|  | 					password: password | ||||||
|  | 				}).then(data => { | ||||||
|  | 					this.update({ | ||||||
|  | 						data: data | ||||||
|  | 					}); | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		this.submit = () => { | ||||||
|  | 			this.api('i/2fa/done', { | ||||||
|  | 				token: this.refs.token.value | ||||||
|  | 			}).then(() => { | ||||||
|  | 				notify('%i18n:desktop.tags.mk-2fa-setting.success%'); | ||||||
|  | 			}).catch(() => { | ||||||
|  | 				notify('%i18n:desktop.tags.mk-2fa-setting.failed%'); | ||||||
|  | 			}); | ||||||
|  | 		}; | ||||||
|  | 	</script> | ||||||
|  | </mk-2fa-setting> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue