This commit is contained in:
syuilo 2018-02-28 00:11:28 +09:00
parent cee5ff23c8
commit 934bf176bc
15 changed files with 272 additions and 377 deletions

View file

@ -0,0 +1,25 @@
/**
* Authorize Form
*/
// Style
import './style.styl';
import init from '../init';
import Index from './views/index.vue';
/**
* init
*/
init(async (launch) => {
document.title = 'Misskey | アプリの連携';
// Launch the app
const [app] = launch();
// Routing
app.$router.addRoutes([
{ path: '/:token', component: Index },
]);
});

View file

@ -42,10 +42,14 @@ export default Vue.extend({
return {
state: null,
session: null,
fetching: true,
token: window.location.href.split('/').pop()
fetching: true
};
},
computed: {
token(): string {
return this.$route.params.token;
}
},
mounted() {
if (!this.$root.$data.os.isSignedIn) return;

View file

@ -5,11 +5,25 @@
// Style
import './style.styl';
require('./tags');
import init from '../init';
import Index from './views/index.vue';
import Apps from './views/apps.vue';
import AppNew from './views/new-app.vue';
import App from './views/app.vue';
/**
* init
*/
init(() => {
init(launch => {
// Launch the app
const [app] = launch();
// Routing
app.$router.addRoutes([
{ path: '/', component: Index },
{ path: '/app', component: Apps },
{ path: '/app/new', component: AppNew },
{ path: '/app/:id', component: App },
]);
});

View file

@ -1,5 +0,0 @@
require('./pages/index.tag');
require('./pages/apps.tag');
require('./pages/app.tag');
require('./pages/new-app.tag');
require('./new-app-form.tag');

View file

@ -1,252 +0,0 @@
<mk-new-app-form>
<form onsubmit={ onsubmit } autocomplete="off">
<section class="name">
<label>
<p class="caption">アプリケーション名</p>
<input ref="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required="required"/>
</label>
</section>
<section class="nid">
<label>
<p class="caption">Named ID</p>
<input ref="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required="required" onkeyup={ onChangeNid }/>
<p class="info" v-if="nidState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%確認しています...</p>
<p class="info" v-if="nidState == 'ok'" style="color:#3CB7B5">%fa:fw check%利用できます</p>
<p class="info" v-if="nidState == 'unavailable'" style="color:#FF1161">%fa:fw exclamation-triangle%既に利用されています</p>
<p class="info" v-if="nidState == 'error'" style="color:#FF1161">%fa:fw exclamation-triangle%通信エラー</p>
<p class="info" v-if="nidState == 'invalid-format'" style="color:#FF1161">%fa:fw exclamation-triangle%a~z、A~Z、0~9、-(ハイフン)が使えます</p>
<p class="info" v-if="nidState == 'min-range'" style="color:#FF1161">%fa:fw exclamation-triangle%3文字以上でお願いします</p>
<p class="info" v-if="nidState == 'max-range'" style="color:#FF1161">%fa:fw exclamation-triangle%30文字以内でお願いします</p>
</label>
</section>
<section class="description">
<label>
<p class="caption">アプリの概要</p>
<textarea ref="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required="required"></textarea>
</label>
</section>
<section class="callback">
<label>
<p class="caption">コールバックURL (オプション)</p>
<input ref="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/>
</label>
</section>
<section class="permission">
<p class="caption">権限</p>
<div ref="permission">
<label>
<input type="checkbox" value="account-read"/>
<p>アカウントの情報を見る。</p>
</label>
<label>
<input type="checkbox" value="account-write"/>
<p>アカウントの情報を操作する。</p>
</label>
<label>
<input type="checkbox" value="post-write"/>
<p>投稿する。</p>
</label>
<label>
<input type="checkbox" value="reaction-write"/>
<p>リアクションしたりリアクションをキャンセルする。</p>
</label>
<label>
<input type="checkbox" value="following-write"/>
<p>フォローしたりフォロー解除する。</p>
</label>
<label>
<input type="checkbox" value="drive-read"/>
<p>ドライブを見る。</p>
</label>
<label>
<input type="checkbox" value="drive-write"/>
<p>ドライブを操作する。</p>
</label>
<label>
<input type="checkbox" value="notification-read"/>
<p>通知を見る。</p>
</label>
<label>
<input type="checkbox" value="notification-write"/>
<p>通知を操作する。</p>
</label>
</div>
<p>%fa:exclamation-triangle%アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。</p>
</section>
<button @click="onsubmit">アプリ作成</button>
</form>
<style lang="stylus" scoped>
:scope
display block
overflow hidden
> form
section
display block
margin 16px 0
.caption
margin 0 0 4px 0
color #616161
font-size 0.95em
> [data-fa]
margin-right 0.25em
color #96adac
.info
display block
margin 4px 0
font-size 0.8em
> [data-fa]
margin-right 0.3em
section.permission
div
padding 8px 0
max-height 160px
overflow auto
background #fff
border solid 1px #cecece
border-radius 4px
label
display block
padding 0 12px
line-height 32px
cursor pointer
&:hover
> p
color #999
[type='checkbox']:checked + p
color #000
[type='checkbox']
margin-right 4px
[type='checkbox']:checked + p
color #111
> p
display inline
color #aaa
user-select none
> p:last-child
margin 6px
font-size 0.8em
color #999
> [data-fa]
margin-right 4px
[type=text]
[type=url]
textarea
user-select text
display inline-block
cursor auto
padding 8px 12px
margin 0
width 100%
font-size 1em
color #333
background #fff
outline none
border solid 1px #cecece
border-radius 4px
&:hover
border-color #bbb
&:focus
border-color $theme-color
&:disabled
opacity 0.5
> button
margin 20px 0 32px 0
width 100%
font-size 1em
color #111
border-radius 3px
</style>
<script lang="typescript">
this.mixin('api');
this.nidState = null;
this.onChangeNid = () => {
const nid = this.$refs.nid.value;
if (nid == '') {
this.update({
nidState: null
});
return;
}
const err =
!nid.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
nid.length < 3 ? 'min-range' :
nid.length > 30 ? 'max-range' :
null;
if (err) {
this.update({
nidState: err
});
return;
}
this.update({
nidState: 'wait'
});
this.$root.$data.os.api('app/name_id/available', {
name_id: nid
}).then(result => {
this.update({
nidState: result.available ? 'ok' : 'unavailable'
});
}).catch(err => {
this.update({
nidState: 'error'
});
});
};
this.onsubmit = () => {
const name = this.$refs.name.value;
const nid = this.$refs.nid.value;
const description = this.$refs.description.value;
const cb = this.$refs.cb.value;
const permission = [];
this.$refs.permission.querySelectorAll('input').forEach(el => {
if (el.checked) permission.push(el.value);
});
const locker = document.body.appendChild(document.createElement('mk-locker'));
this.$root.$data.os.api('app/create', {
name: name,
name_id: nid,
description: description,
callback_url: cb,
permission: permission
}).then(() => {
location.href = '/apps';
}).catch(() => {
alert('アプリの作成に失敗しました。再度お試しください。');
locker.parentNode.removeChild(locker);
});
};
</script>
</mk-new-app-form>

View file

@ -1,32 +0,0 @@
<mk-app-page>
<p v-if="fetching">読み込み中</p>
<main v-if="!fetching">
<header>
<h1>{ app.name }</h1>
</header>
<div class="body">
<p>App Secret</p>
<input value={ app.secret } readonly="readonly"/>
</div>
</main>
<style lang="stylus" scoped>
:scope
display block
</style>
<script lang="typescript">
this.mixin('api');
this.fetching = true;
this.on('mount', () => {
this.$root.$data.os.api('app/show', {
app_id: this.opts.app
}).then(app => {
this.update({
fetching: false,
app: app
});
});
});
</script>
</mk-app-page>

View file

@ -1,33 +0,0 @@
<mk-apps-page>
<h1>アプリを管理</h1><a href="/app/new">アプリ作成</a>
<div class="apps">
<p v-if="fetching">読み込み中</p>
<template v-if="!fetching">
<p v-if="apps.length == 0">アプリなし</p>
<ul v-if="apps.length > 0">
<li each={ app in apps }><a href={ '/app/' + app.id }>
<p class="name">{ app.name }</p></a></li>
</ul>
</template>
</div>
<style lang="stylus" scoped>
:scope
display block
</style>
<script lang="typescript">
this.mixin('api');
this.fetching = true;
this.on('mount', () => {
this.$root.$data.os.api('my/apps').then(apps => {
this.fetching = false
this.apps = apps
this.update({
fetching: false,
apps: apps
});
});
});
</script>
</mk-apps-page>

View file

@ -1,6 +0,0 @@
<mk-index><a href="/apps">アプリ</a>
<style lang="stylus" scoped>
:scope
display block
</style>
</mk-index>

View file

@ -1,42 +0,0 @@
<mk-new-app-page>
<main>
<header>
<h1>新しいアプリを作成</h1>
<p>MisskeyのAPIを利用したアプリケーションを作成できます。</p>
</header>
<mk-new-app-form/>
</main>
<style lang="stylus" scoped>
:scope
display block
padding 64px 0
> main
width 100%
max-width 700px
margin 0 auto
> header
margin 0 0 16px 0
padding 0 0 16px 0
border-bottom solid 1px #282827
> h1
margin 0 0 12px 0
padding 0
line-height 32px
font-size 32px
font-weight normal
color #000
> p
margin 0
line-height 16px
color #9a9894
</style>
</mk-new-app-page>

View file

@ -0,0 +1,43 @@
<template>
<div>
<p v-if="fetching">読み込み中</p>
<main v-if="!fetching">
<header>
<h1>{{ app.name }}</h1>
</header>
<div class="body">
<p>App Secret</p>
<input :value="app.secret" readonly/>
</div>
</main>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
fetching: true,
app: null
};
},
watch: {
$route: 'fetch'
},
mounted() {
this.fetch();
},
methods: {
fetch() {
this.fetching = true;
(this as any).api('app/show', {
app_id: this.$route.params.id
}).then(app => {
this.app = app;
this.fetching = false;
});
}
}
});
</script>

View file

@ -0,0 +1,24 @@
<template>
<div>
<h1>アプリを管理</h1>
<router-link to="/app/new">アプリ作成</router-link>
<div class="apps">
<p v-if="fetching">読み込み中</p>
<template v-if="!fetching">
<p v-if="apps.length == 0">アプリなし</p>
<ul v-else>
<li v-for="app in apps" :key="app.id">
<router-link :to="`/app/${app.id}`">
<p class="name">{{ app.name }}</p>
</router-link>
</li>
</ul>
</template>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend();
</script>

View file

@ -0,0 +1,10 @@
<template>
<div>
<router-link to="/app">アプリ</router-link>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend();
</script>

View file

@ -0,0 +1,145 @@
<template>
<div>
<form @submit="onSubmit" autocomplete="off">
<section class="name">
<label>
<p class="caption">アプリケーション名</p>
<input v-model="name" type="text" placeholder="ex) Misskey for iOS" autocomplete="off" required/>
</label>
</section>
<section class="nid">
<label>
<p class="caption">Named ID</p>
<input v-model="nid" type="text" pattern="^[a-zA-Z0-9-]{3,30}$" placeholder="ex) misskey-for-ios" autocomplete="off" required/>
<p class="info" v-if="nidState == 'wait'" style="color:#999">%fa:spinner .pulse .fw%確認しています...</p>
<p class="info" v-if="nidState == 'ok'" style="color:#3CB7B5">%fa:fw check%利用できます</p>
<p class="info" v-if="nidState == 'unavailable'" style="color:#FF1161">%fa:fw exclamation-triangle%既に利用されています</p>
<p class="info" v-if="nidState == 'error'" style="color:#FF1161">%fa:fw exclamation-triangle%通信エラー</p>
<p class="info" v-if="nidState == 'invalid-format'" style="color:#FF1161">%fa:fw exclamation-triangle%a~zA~Z0~9-(ハイフン)が使えます</p>
<p class="info" v-if="nidState == 'min-range'" style="color:#FF1161">%fa:fw exclamation-triangle%3文字以上でお願いします</p>
<p class="info" v-if="nidState == 'max-range'" style="color:#FF1161">%fa:fw exclamation-triangle%30文字以内でお願いします</p>
</label>
</section>
<section class="description">
<label>
<p class="caption">アプリの概要</p>
<textarea v-model="description" placeholder="ex) Misskey iOSクライアント。" autocomplete="off" required></textarea>
</label>
</section>
<section class="callback">
<label>
<p class="caption">コールバックURL (オプション)</p>
<input v-model="cb" type="url" placeholder="ex) https://your.app.example.com/callback.php" autocomplete="off"/>
</label>
</section>
<section class="permission">
<p class="caption">権限</p>
<div ref="permission">
<label>
<input type="checkbox" value="account-read"/>
<p>アカウントの情報を見る</p>
</label>
<label>
<input type="checkbox" value="account-write"/>
<p>アカウントの情報を操作する</p>
</label>
<label>
<input type="checkbox" value="post-write"/>
<p>投稿する</p>
</label>
<label>
<input type="checkbox" value="reaction-write"/>
<p>リアクションしたりリアクションをキャンセルする</p>
</label>
<label>
<input type="checkbox" value="following-write"/>
<p>フォローしたりフォロー解除する</p>
</label>
<label>
<input type="checkbox" value="drive-read"/>
<p>ドライブを見る</p>
</label>
<label>
<input type="checkbox" value="drive-write"/>
<p>ドライブを操作する</p>
</label>
<label>
<input type="checkbox" value="notification-read"/>
<p>通知を見る</p>
</label>
<label>
<input type="checkbox" value="notification-write"/>
<p>通知を操作する</p>
</label>
</div>
<p>%fa:exclamation-triangle%アプリ作成後も変更できますが新たな権限を付与する場合その時点で関連付けられているユーザーキーはすべて無効になります</p>
</section>
<button type="submit">アプリ作成</button>
</form>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
name: '',
nid: '',
description: '',
cb: '',
nidState: null
};
},
watch: {
nid() {
if (this.nid == null || this.nid == '') {
this.nidState = null;
return;
}
const err =
!this.nid.match(/^[a-zA-Z0-9\-]+$/) ? 'invalid-format' :
this.nid.length < 3 ? 'min-range' :
this.nid.length > 30 ? 'max-range' :
null;
if (err) {
this.nidState = err;
return;
}
this.nidState = 'wait';
(this as any).api('app/name_id/available', {
name_id: this.nid
}).then(result => {
this.nidState = result.available ? 'ok' : 'unavailable';
}).catch(err => {
this.nidState = 'error';
});
}
},
methods: {
onSubmit() {
const permission = [];
(this.$refs.permission as any).querySelectorAll('input').forEach(el => {
if (el.checked) permission.push(el.value);
});
(this as any).api('app/create', {
name: this.name,
name_id: this.nid,
description: this.description,
callback_url: this.cb,
permission: permission
}).then(() => {
location.href = '/apps';
}).catch(() => {
alert('アプリの作成に失敗しました。再度お試しください。');
});
}
}
});
</script>

View file

@ -77,7 +77,7 @@ if (localStorage.getItem('should-refresh') == 'true') {
}
// MiOSを初期化してコールバックする
export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => {
export default (callback: (launch: (api?: (os: MiOS) => API) => [Vue, MiOS]) => void, sw = false) => {
const os = new MiOS(sw);
os.init(() => {

View file

@ -38,8 +38,8 @@ module.exports = Object.keys(langs).map(lang => {
//ch: './src/web/app/ch/script.ts',
//stats: './src/web/app/stats/script.ts',
//status: './src/web/app/status/script.ts',
//dev: './src/web/app/dev/script.ts',
//auth: './src/web/app/auth/script.ts',
dev: './src/web/app/dev/script.ts',
auth: './src/web/app/auth/script.ts',
sw: './src/web/app/sw.js'
};