diff --git a/src/web/app/auth/tags/form.tag b/src/web/app/auth/tags/form.tag
deleted file mode 100644
index b1de0baab6..0000000000
--- a/src/web/app/auth/tags/form.tag
+++ /dev/null
@@ -1,130 +0,0 @@
-<mk-form>
-	<header>
-		<h1><i>{ app.name }</i>があなたの<b>アカウント</b>に<b>アクセス</b>することを<b>許可</b>しますか?</h1><img src={ app.icon_url + '?thumbnail&size=64' }/>
-	</header>
-	<div class="app">
-		<section>
-			<h2>{ app.name }</h2>
-			<p class="nid">{ app.name_id }</p>
-			<p class="description">{ app.description }</p>
-		</section>
-		<section>
-			<h2>このアプリは次の権限を要求しています:</h2>
-			<ul>
-				<template each={ p in app.permission }>
-					<li v-if="p == 'account-read'">アカウントの情報を見る。</li>
-					<li v-if="p == 'account-write'">アカウントの情報を操作する。</li>
-					<li v-if="p == 'post-write'">投稿する。</li>
-					<li v-if="p == 'like-write'">いいねしたりいいね解除する。</li>
-					<li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li>
-					<li v-if="p == 'drive-read'">ドライブを見る。</li>
-					<li v-if="p == 'drive-write'">ドライブを操作する。</li>
-					<li v-if="p == 'notification-read'">通知を見る。</li>
-					<li v-if="p == 'notification-write'">通知を操作する。</li>
-				</template>
-			</ul>
-		</section>
-	</div>
-	<div class="action">
-		<button @click="cancel">キャンセル</button>
-		<button @click="accept">アクセスを許可</button>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> header
-				> h1
-					margin 0
-					padding 32px 32px 20px 32px
-					font-size 24px
-					font-weight normal
-					color #777
-
-					i
-						color #77aeca
-
-						&:before
-							content '「'
-
-						&:after
-							content '」'
-
-					b
-						color #666
-
-				> img
-					display block
-					z-index 1
-					width 84px
-					height 84px
-					margin 0 auto -38px auto
-					border solid 5px #fff
-					border-radius 100%
-					box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
-
-			> .app
-				padding 44px 16px 0 16px
-				color #555
-				background #eee
-				box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
-
-				&:after
-					content ''
-					display block
-					clear both
-
-				> section
-					float left
-					width 50%
-					padding 8px
-					text-align left
-
-					> h2
-						margin 0
-						font-size 16px
-						color #777
-
-			> .action
-				padding 16px
-
-				> button
-					margin 0 8px
-
-			@media (max-width 600px)
-				> header
-					> img
-						box-shadow none
-
-				> .app
-					box-shadow none
-
-			@media (max-width 500px)
-				> header
-					> h1
-						font-size 16px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.session = this.opts.session;
-		this.app = this.session.app;
-
-		this.cancel = () => {
-			this.$root.$data.os.api('auth/deny', {
-				token: this.session.token
-			}).then(() => {
-				this.$emit('denied');
-			});
-		};
-
-		this.accept = () => {
-			this.$root.$data.os.api('auth/accept', {
-				token: this.session.token
-			}).then(() => {
-				this.$emit('accepted');
-			});
-		};
-	</script>
-</mk-form>
diff --git a/src/web/app/auth/tags/index.tag b/src/web/app/auth/tags/index.tag
deleted file mode 100644
index 56fbbb7da2..0000000000
--- a/src/web/app/auth/tags/index.tag
+++ /dev/null
@@ -1,143 +0,0 @@
-<mk-index>
-	<main v-if="$root.$data.os.isSignedIn">
-		<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
-		<mk-form ref="form" v-if="state == 'waiting'" session={ session }/>
-		<div class="denied" v-if="state == 'denied'">
-			<h1>アプリケーションの連携をキャンセルしました。</h1>
-			<p>このアプリがあなたのアカウントにアクセスすることはありません。</p>
-		</div>
-		<div class="accepted" v-if="state == 'accepted'">
-			<h1>{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}</h1>
-			<p v-if="session.app.callback_url">アプリケーションに戻っています<mk-ellipsis/></p>
-			<p v-if="!session.app.callback_url">アプリケーションに戻って、やっていってください。</p>
-		</div>
-		<div class="error" v-if="state == 'fetch-session-error'">
-			<p>セッションが存在しません。</p>
-		</div>
-	</main>
-	<main class="signin" v-if="!$root.$data.os.isSignedIn">
-		<h1>サインインしてください</h1>
-		<mk-signin/>
-	</main>
-	<footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> main
-				width 100%
-				max-width 500px
-				margin 0 auto
-				text-align center
-				background #fff
-				box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
-
-				> .fetching
-					margin 0
-					padding 32px
-					color #555
-
-				> div
-					padding 64px
-
-					> h1
-						margin 0 0 8px 0
-						padding 0
-						font-size 20px
-						font-weight normal
-
-					> p
-						margin 0
-						color #555
-
-					&.denied > h1
-						color #e65050
-
-					&.accepted > h1
-						color #54af7c
-
-				&.signin
-					padding 32px 32px 16px 32px
-
-					> h1
-						margin 0 0 22px 0
-						padding 0
-						font-size 20px
-						font-weight normal
-						color #555
-
-				@media (max-width 600px)
-					max-width none
-					box-shadow none
-
-				@media (max-width 500px)
-					> div
-						> h1
-							font-size 16px
-
-			> footer
-				> img
-					display block
-					width 64px
-					height 64px
-					margin 0 auto
-
-	</style>
-	<script lang="typescript">
-		this.mixin('i');
-		this.mixin('api');
-
-		this.state = null;
-		this.fetching = true;
-
-		this.token = window.location.href.split('/').pop();
-
-		this.on('mount', () => {
-			if (!this.$root.$data.os.isSignedIn) return;
-
-			// Fetch session
-			this.$root.$data.os.api('auth/session/show', {
-				token: this.token
-			}).then(session => {
-				this.session = session;
-				this.fetching = false;
-
-				// 既に連携していた場合
-				if (this.session.app.is_authorized) {
-					this.$root.$data.os.api('auth/accept', {
-						token: this.session.token
-					}).then(() => {
-						this.accepted();
-					});
-				} else {
-					this.update({
-						state: 'waiting'
-					});
-
-					this.$refs.form.on('denied', () => {
-						this.update({
-							state: 'denied'
-						});
-					});
-
-					this.$refs.form.on('accepted', this.accepted);
-				}
-			}).catch(error => {
-				this.update({
-					fetching: false,
-					state: 'fetch-session-error'
-				});
-			});
-		});
-
-		this.accepted = () => {
-			this.update({
-				state: 'accepted'
-			});
-
-			if (this.session.app.callback_url) {
-				location.href = this.session.app.callback_url + '?token=' + this.session.token;
-			}
-		};
-	</script>
-</mk-index>
diff --git a/src/web/app/auth/tags/index.ts b/src/web/app/auth/tags/index.ts
deleted file mode 100644
index 42dffe67d9..0000000000
--- a/src/web/app/auth/tags/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-require('./index.tag');
-require('./form.tag');
diff --git a/src/web/app/auth/views/form.vue b/src/web/app/auth/views/form.vue
new file mode 100644
index 0000000000..30ad64ed2d
--- /dev/null
+++ b/src/web/app/auth/views/form.vue
@@ -0,0 +1,140 @@
+<template>
+<div class="form">
+	<header>
+		<h1><i>{{ app.name }}</i>があなたのアカウントにアクセスすることを<b>許可</b>しますか?</h1>
+		<img :src="`${app.icon_url}?thumbnail&size=64`"/>
+	</header>
+	<div class="app">
+		<section>
+			<h2>{{ app.name }}</h2>
+			<p class="nid">{{ app.name_id }}</p>
+			<p class="description">{{ app.description }}</p>
+		</section>
+		<section>
+			<h2>このアプリは次の権限を要求しています:</h2>
+			<ul>
+				<template v-for="p in app.permission">
+					<li v-if="p == 'account-read'">アカウントの情報を見る。</li>
+					<li v-if="p == 'account-write'">アカウントの情報を操作する。</li>
+					<li v-if="p == 'post-write'">投稿する。</li>
+					<li v-if="p == 'like-write'">いいねしたりいいね解除する。</li>
+					<li v-if="p == 'following-write'">フォローしたりフォロー解除する。</li>
+					<li v-if="p == 'drive-read'">ドライブを見る。</li>
+					<li v-if="p == 'drive-write'">ドライブを操作する。</li>
+					<li v-if="p == 'notification-read'">通知を見る。</li>
+					<li v-if="p == 'notification-write'">通知を操作する。</li>
+				</template>
+			</ul>
+		</section>
+	</div>
+	<div class="action">
+		<button @click="cancel">キャンセル</button>
+		<button @click="accept">アクセスを許可</button>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['session'],
+	computed: {
+		app(): any {
+			return this.session.app;
+		}
+	},
+	methods: {
+		cancel() {
+			(this as any).api('auth/deny', {
+				token: this.session.token
+			}).then(() => {
+				this.$emit('denied');
+			});
+		},
+
+		accept() {
+			(this as any).api('auth/accept', {
+				token: this.session.token
+			}).then(() => {
+				this.$emit('accepted');
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.form
+
+	> header
+		> h1
+			margin 0
+			padding 32px 32px 20px 32px
+			font-size 24px
+			font-weight normal
+			color #777
+
+			i
+				color #77aeca
+
+				&:before
+					content '「'
+
+				&:after
+					content '」'
+
+			b
+				color #666
+
+		> img
+			display block
+			z-index 1
+			width 84px
+			height 84px
+			margin 0 auto -38px auto
+			border solid 5px #fff
+			border-radius 100%
+			box-shadow 0 2px 2px rgba(0, 0, 0, 0.1)
+
+	> .app
+		padding 44px 16px 0 16px
+		color #555
+		background #eee
+		box-shadow 0 2px 2px rgba(0, 0, 0, 0.1) inset
+
+		&:after
+			content ''
+			display block
+			clear both
+
+		> section
+			float left
+			width 50%
+			padding 8px
+			text-align left
+
+			> h2
+				margin 0
+				font-size 16px
+				color #777
+
+	> .action
+		padding 16px
+
+		> button
+			margin 0 8px
+
+	@media (max-width 600px)
+		> header
+			> img
+				box-shadow none
+
+		> .app
+			box-shadow none
+
+	@media (max-width 500px)
+		> header
+			> h1
+				font-size 16px
+
+</style>
diff --git a/src/web/app/auth/views/index.vue b/src/web/app/auth/views/index.vue
new file mode 100644
index 0000000000..56a7bac7a3
--- /dev/null
+++ b/src/web/app/auth/views/index.vue
@@ -0,0 +1,145 @@
+<template>
+<div class="index">
+	<main v-if="os.isSignedIn">
+		<p class="fetching" v-if="fetching">読み込み中<mk-ellipsis/></p>
+		<fo-rm
+			ref="form"
+			v-if="state == 'waiting'"
+			:session="session"
+			@denied="state = 'denied'"
+			@accepted="accepted"
+		/>
+		<div class="denied" v-if="state == 'denied'">
+			<h1>アプリケーションの連携をキャンセルしました。</h1>
+			<p>このアプリがあなたのアカウントにアクセスすることはありません。</p>
+		</div>
+		<div class="accepted" v-if="state == 'accepted'">
+			<h1>{{ session.app.is_authorized ? 'このアプリは既に連携済みです' : 'アプリケーションの連携を許可しました'}}</h1>
+			<p v-if="session.app.callback_url">アプリケーションに戻っています<mk-ellipsis/></p>
+			<p v-if="!session.app.callback_url">アプリケーションに戻って、やっていってください。</p>
+		</div>
+		<div class="error" v-if="state == 'fetch-session-error'">
+			<p>セッションが存在しません。</p>
+		</div>
+	</main>
+	<main class="signin" v-if="!os.isSignedIn">
+		<h1>サインインしてください</h1>
+		<mk-signin/>
+	</main>
+	<footer><img src="/assets/auth/logo.svg" alt="Misskey"/></footer>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Form from './form.vue';
+
+export default Vue.extend({
+	components: {
+		'fo-rm': Form
+	},
+	data() {
+		return {
+			state: null,
+			session: null,
+			fetching: true,
+			token: window.location.href.split('/').pop()
+		};
+	},
+	mounted() {
+		if (!this.$root.$data.os.isSignedIn) return;
+
+		// Fetch session
+		(this as any).api('auth/session/show', {
+			token: this.token
+		}).then(session => {
+			this.session = session;
+			this.fetching = false;
+
+			// 既に連携していた場合
+			if (this.session.app.is_authorized) {
+				this.$root.$data.os.api('auth/accept', {
+					token: this.session.token
+				}).then(() => {
+					this.accepted();
+				});
+			} else {
+				this.state = 'waiting';
+			}
+		}).catch(error => {
+			this.state = 'fetch-session-error';
+		});
+	},
+	methods: {
+		accepted() {
+			this.state = 'accepted';
+			if (this.session.app.callback_url) {
+				location.href = this.session.app.callback_url + '?token=' + this.session.token;
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.index
+
+	> main
+		width 100%
+		max-width 500px
+		margin 0 auto
+		text-align center
+		background #fff
+		box-shadow 0px 4px 16px rgba(0, 0, 0, 0.2)
+
+		> .fetching
+			margin 0
+			padding 32px
+			color #555
+
+		> div
+			padding 64px
+
+			> h1
+				margin 0 0 8px 0
+				padding 0
+				font-size 20px
+				font-weight normal
+
+			> p
+				margin 0
+				color #555
+
+			&.denied > h1
+				color #e65050
+
+			&.accepted > h1
+				color #54af7c
+
+		&.signin
+			padding 32px 32px 16px 32px
+
+			> h1
+				margin 0 0 22px 0
+				padding 0
+				font-size 20px
+				font-weight normal
+				color #555
+
+		@media (max-width 600px)
+			max-width none
+			box-shadow none
+
+		@media (max-width 500px)
+			> div
+				> h1
+					font-size 16px
+
+	> footer
+		> img
+			display block
+			width 64px
+			height 64px
+			margin 0 auto
+
+</style>