diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 40772f9b1fcddafc985986d0645dea47121a6c34..4650f03a29aae74e956d6e2c083a433ba3add1c3 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -568,7 +568,11 @@ _permissions: _auth: shareAccess: "「{name}ã€ãŒã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã“ã¨ã‚’許å¯ã—ã¾ã™ã‹ï¼Ÿ" + shareAccessAsk: "アカウントã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’許å¯ã—ã¾ã™ã‹ï¼Ÿ" permissionAsk: "ã“ã®ã‚¢ãƒ—リã¯æ¬¡ã®æ¨©é™ã‚’è¦æ±‚ã—ã¦ã„ã¾ã™" + pleaseGoBack: "アプリケーションã«æˆ»ã£ã¦ã‚„ã£ã¦ã„ã£ã¦ãã ã•ã„" + callback: "アプリケーションã«æˆ»ã£ã¦ã„ã¾ã™" + denied: "アクセスを拒å¦ã—ã¾ã—ãŸ" _antennaSources: all: "å…¨ã¦ã®ãƒŽãƒ¼ãƒˆ" diff --git a/migration/1585361548360-miauth.ts b/migration/1585361548360-miauth.ts new file mode 100644 index 0000000000000000000000000000000000000000..22d6c207ff0f9230ab6f14aaea336d0639df0b58 --- /dev/null +++ b/migration/1585361548360-miauth.ts @@ -0,0 +1,36 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class miauth1585361548360 implements MigrationInterface { + name = 'miauth1585361548360' + + public async up(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "access_token" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE DEFAULT null`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ADD "session" character varying(128) DEFAULT null`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ADD "name" character varying(128) DEFAULT null`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ADD "description" character varying(512) DEFAULT null`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ADD "iconUrl" character varying(512) DEFAULT null`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ADD "permission" character varying(64) array NOT NULL DEFAULT '{}'::varchar[]`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ADD "fetched" boolean NOT NULL DEFAULT false`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP NOT NULL`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET DEFAULT null`, undefined); + await queryRunner.query(`CREATE INDEX "IDX_bf3a053c07d9fb5d87317c56ee" ON "access_token" ("session") `, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<any> { + await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_bf3a053c07d9fb5d87317c56ee"`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET NOT NULL`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "fetched"`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "permission"`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "iconUrl"`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "description"`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "name"`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "session"`, undefined); + await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "lastUsedAt"`, undefined); + } + +} diff --git a/src/client/pages/auth.vue b/src/client/pages/auth.vue index 9f5b45f0017405edd8b7fd363f0ea6e6684d05a5..e025924fe025be10868aa6bfaf162d3feda42e0e 100755 --- a/src/client/pages/auth.vue +++ b/src/client/pages/auth.vue @@ -12,20 +12,18 @@ @accepted="accepted" /> <div class="denied _panel" v-if="state == 'denied'"> - <h1>{{ $t('denied') }}</h1> - <p>{{ $t('denied-paragraph') }}</p> + <h1>{{ $t('_auth.denied') }}</h1> </div> <div class="accepted _panel" v-if="state == 'accepted'"> <h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1> - <p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p> - <p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p> + <p v-if="session.app.callbackUrl">{{ $t('_auth.callback') }}<mk-ellipsis/></p> + <p v-if="!session.app.callbackUrl">{{ $t('_auth.pleaseGoBack') }}</p> </div> <div class="error _panel" v-if="state == 'fetch-session-error'"> <p>{{ $t('error') }}</p> </div> </div> <div class="signin" v-else> - <h1>{{ $t('sign-in') }}</h1> <mk-signin @login="onLogin"/> </div> </template> diff --git a/src/client/pages/doc.vue b/src/client/pages/doc.vue index 5cdb5999ba289b7ef4c190d16dd1f128c8e21283..8d1ba4bd73db06dc8845af24e726ddef7c57bb1a 100644 --- a/src/client/pages/doc.vue +++ b/src/client/pages/doc.vue @@ -118,6 +118,10 @@ export default Vue.extend({ margin-bottom: 0; } + ::v-deep a { + color: var(--link); + } + ::v-deep h2 { font-size: 1.25em; padding: 0 0 0.5em 0; diff --git a/src/client/pages/miauth.vue b/src/client/pages/miauth.vue new file mode 100644 index 0000000000000000000000000000000000000000..7d7e363d39c115e67dcab428cee7a06dd7b12870 --- /dev/null +++ b/src/client/pages/miauth.vue @@ -0,0 +1,99 @@ +<template> +<div v-if="$store.getters.isSignedIn"> + <div class="waiting _card" v-if="state == 'waiting'"> + <div class="_content"> + <mk-loading/> + </div> + </div> + <div class="denied _card" v-if="state == 'denied'"> + <div class="_content"> + <p>{{ $t('_auth.denied') }}</p> + </div> + </div> + <div class="accepted _card" v-else-if="state == 'accepted'"> + <div class="_content"> + <p v-if="callback">{{ $t('_auth.callback') }}<mk-ellipsis/></p> + <p v-else>{{ $t('_auth.pleaseGoBack') }}</p> + </div> + </div> + <div class="_card" v-else> + <div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div> + <div class="_title" v-else>{{ $t('_auth.shareAccessAsk') }}</div> + <div class="_content"> + <p>{{ $t('_auth.permissionAsk') }}</p> + <ul> + <template v-for="p in permission"> + <li :key="p">{{ $t(`_permissions.${p}`) }}</li> + </template> + </ul> + </div> + <div class="_footer"> + <mk-button @click="deny" inline>{{ $t('cancel') }}</mk-button> + <mk-button @click="accept" inline primary>{{ $t('accept') }}</mk-button> + </div> + </div> +</div> +<div class="signin" v-else> + <mk-signin @login="onLogin"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../i18n'; +import MkSignin from '../components/signin.vue'; +import MkButton from '../components/ui/button.vue'; + +export default Vue.extend({ + i18n, + components: { + MkSignin, + MkButton, + }, + data() { + return { + state: null + }; + }, + computed: { + session(): string { + return this.$route.params.session; + }, + callback(): string { + return this.$route.query.callback; + }, + name(): string { + return this.$route.query.name; + }, + permission(): string { + return this.$route.query.permission; + }, + }, + methods: { + async accept() { + this.state = 'waiting'; + await this.$root.api('miauth/gen-token', { + session: this.session, + name: this.name, + permission: this.permission || [], + }); + + this.state = 'accepted'; + if (this.callback) { + location.href = `${this.callback}?session=${this.session}`; + } + }, + deny() { + this.state = 'denied'; + }, + onLogin(res) { + localStorage.setItem('i', res.i); + location.reload(); + } + } +}); +</script> + +<style lang="scss" scoped> + +</style> diff --git a/src/client/router.ts b/src/client/router.ts index 83445fea7e8a5f0d5da0d464cf59aa869b5f7b95..70d497d36f7d64b252d0db83baa0fd53f226b918 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -58,6 +58,7 @@ export const router = new VueRouter({ { path: '/notes/:note', name: 'note', component: page('note') }, { path: '/tags/:tag', component: page('tag') }, { path: '/auth/:token', component: page('auth') }, + { path: '/miauth/:session', component: page('miauth') }, { path: '/authorize-follow', component: page('follow') }, { path: '/share', component: page('share') }, { path: '*', component: page('not-found') } diff --git a/src/docs/api.ja-JP.md b/src/docs/api.ja-JP.md index 5d370a29cd892a3c4d91e6f7cb7b0a676fea42cc..f47aeb81cf1813c61f9372021d772ecd46601017 100644 --- a/src/docs/api.ja-JP.md +++ b/src/docs/api.ja-JP.md @@ -3,25 +3,25 @@ MisskeyAPIを使ã£ã¦Misskeyクライアントã€Misskey連æºWebサービスã€Botç‰(以下「アプリケーションã€ã¨å‘¼ã³ã¾ã™)を開発ã§ãã¾ã™ã€‚ ストリーミングAPIã‚‚ã‚ã‚‹ã®ã§ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ 性ã®ã‚るアプリケーションを作るã“ã¨ã‚‚å¯èƒ½ã§ã™ã€‚ -APIを使ã„始ã‚ã‚‹ã«ã¯ã€ã¾ãšAPIã‚ーをå–å¾—ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ -ã“ã®ãƒ‰ã‚ュメントã§ã¯ã€APIã‚ーをå–å¾—ã™ã‚‹æ‰‹é †ã‚’説明ã—ãŸå¾Œã€åŸºæœ¬çš„ãªAPIã®ä½¿ã„方を説明ã—ã¾ã™ã€‚ +APIを使ã„始ã‚ã‚‹ã«ã¯ã€ã¾ãšã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +ã“ã®ãƒ‰ã‚ュメントã§ã¯ã€ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã™ã‚‹æ‰‹é †ã‚’説明ã—ãŸå¾Œã€åŸºæœ¬çš„ãªAPIã®ä½¿ã„方を説明ã—ã¾ã™ã€‚ -## APIã‚ーã®å–å¾— -基本的ã«ã€APIã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ã¯APIã‚ーãŒå¿…è¦ã¨ãªã‚Šã¾ã™ã€‚ -ã‚ãªãŸã®ä½œã‚ã†ã¨ã—ã¦ã„るアプリケーションãŒã€ã‚ãªãŸå°‚用ã®ã‚‚ã®ãªã®ã‹ã€ãã‚Œã¨ã‚‚ä¸ç‰¹å®šå¤šæ•°ã®äººã«ä½¿ã£ã¦ã‚‚らã†ã‚‚ã®ãªã®ã‹ã«ã‚ˆã£ã¦ã€APIã‚ーã®å–å¾—æ‰‹é †ã¯ç•°ãªã‚Šã¾ã™ã€‚ +## アクセストークンã®å–å¾— +基本的ã«ã€APIã¯ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ã¯ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ãŒå¿…è¦ã¨ãªã‚Šã¾ã™ã€‚ +ã‚ãªãŸã®ä½œã‚ã†ã¨ã—ã¦ã„るアプリケーションãŒã€ã‚ãªãŸå°‚用ã®ã‚‚ã®ãªã®ã‹ã€ãã‚Œã¨ã‚‚ä¸ç‰¹å®šå¤šæ•°ã®äººã«ä½¿ã£ã¦ã‚‚らã†ã‚‚ã®ãªã®ã‹ã«ã‚ˆã£ã¦ã€ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã®å–å¾—æ‰‹é †ã¯ç•°ãªã‚Šã¾ã™ã€‚ -* ã‚ãªãŸå°‚用ã®å ´åˆ: [「自分ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®APIã‚ーをå–å¾—ã™ã‚‹ã€](#自分ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®APIã‚ーをå–å¾—ã™ã‚‹)ã«é€²ã‚€ -* 皆ã«ä½¿ã£ã¦ã‚‚らã†å ´åˆ: [「アプリケーションã¨ã—ã¦APIã‚ーをå–å¾—ã™ã‚‹ã€](#アプリケーションã¨ã—ã¦APIã‚ーをå–å¾—ã™ã‚‹)ã«é€²ã‚€ +* ã‚ãªãŸå°‚用ã®å ´åˆ: [「自分ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã™ã‚‹ã€](#自分ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã™ã‚‹)ã«é€²ã‚€ +* 皆ã«ä½¿ã£ã¦ã‚‚らã†å ´åˆ: [「アプリケーションã¨ã—ã¦ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã™ã‚‹ã€](#アプリケーションã¨ã—ã¦ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã™ã‚‹)ã«é€²ã‚€ -### 自分ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®APIã‚ーをå–å¾—ã™ã‚‹ -「è¨å®š > APIã€ã§ã€è‡ªåˆ†ã®APIã‚ーをå–å¾—ã§ãã¾ã™ã€‚ +### 自分ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã™ã‚‹ +「è¨å®š > APIã€ã§ã€è‡ªåˆ†ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã§ãã¾ã™ã€‚ -> ã“ã®æ–¹æ³•ã§å…¥æ‰‹ã—ãŸAPIã‚ーã¯å¼·åŠ›ãªã®ã§ã€ç¬¬ä¸‰è€…ã«æ•™ãˆãªã„ã§ãã ã•ã„(アプリãªã©ã«ã‚‚入力ã—ãªã„ã§ãã ã•ã„)。 +> ã“ã®æ–¹æ³•ã§å…¥æ‰‹ã—ãŸã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã¯å¼·åŠ›ãªã®ã§ã€ç¬¬ä¸‰è€…ã«æ•™ãˆãªã„ã§ãã ã•ã„(アプリãªã©ã«ã‚‚入力ã—ãªã„ã§ãã ã•ã„)。 [「APIã®ä½¿ã„æ–¹ã€ã¸é€²ã‚€](#APIã®ä½¿ã„æ–¹) -### アプリケーションã¨ã—ã¦APIã‚ーをå–å¾—ã™ã‚‹ -アプリケーションを使ã£ã¦ã‚‚らã†ã«ã¯ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®APIã‚ーを以下ã®æ‰‹é †ã§å–å¾—ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ +### アプリケーションã¨ã—ã¦ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å–å¾—ã™ã‚‹ +アプリケーションを使ã£ã¦ã‚‚らã†ã«ã¯ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’以下ã®æ‰‹é †ã§å–å¾—ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ #### Step 1 @@ -38,23 +38,23 @@ UUIDを生æˆã™ã‚‹ã€‚以後ã“れをセッションIDã¨å‘¼ã³ã¾ã™ã€‚ * `callback` ... èªè¨¼ãŒçµ‚ã‚ã£ãŸå¾Œã«ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã™ã‚‹URL * > 例: `https://missdeck.example.com/callback` * リダイレクト時ã«ã¯ã€`session`ã¨ã„ã†ã‚¯ã‚¨ãƒªãƒ‘ラメータã§ã‚»ãƒƒã‚·ãƒ§ãƒ³IDãŒä»˜ãã¾ã™ -* `permissions` ... アプリケーションãŒè¦æ±‚ã™ã‚‹æ¨©é™ +* `permission` ... アプリケーションãŒè¦æ±‚ã™ã‚‹æ¨©é™ * > 例: `write:notes,write:following,read:drive` * è¦æ±‚ã™ã‚‹æ¨©é™ã‚’`,`ã§åŒºåˆ‡ã£ã¦åˆ—挙ã—ã¾ã™ * ã©ã®ã‚ˆã†ãªæ¨©é™ãŒã‚ã‚‹ã‹ã¯[APIリファレンス](/api-doc)ã§ç¢ºèªã§ãã¾ã™ #### Step 3 -ユーザーãŒé€£æºã‚’許å¯ã—ãŸå¾Œã€`{_URL_}/miauth/{session}/check`ã«POSTリクエストã™ã‚‹ã¨ã€ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã¨ã—ã¦APIã‚ーをå«ã‚€JSONãŒè¿”ã‚Šã¾ã™ã€‚ +ユーザーãŒé€£æºã‚’許å¯ã—ãŸå¾Œã€`{_URL_}/miauth/{session}/check`ã«POSTリクエストã™ã‚‹ã¨ã€ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã¨ã—ã¦ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’å«ã‚€JSONãŒè¿”ã‚Šã¾ã™ã€‚ レスãƒãƒ³ã‚¹ã«å«ã¾ã‚Œã‚‹ãƒ—ãƒãƒ‘ティ: -* `token` ... ユーザーã®APIã‚ー +* `token` ... ユーザーã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ * `user` ... ユーザーã®æƒ…å ± [「APIã®ä½¿ã„æ–¹ã€ã¸é€²ã‚€](#APIã®ä½¿ã„æ–¹) ## APIã®ä½¿ã„æ–¹ **APIã¯ã™ã¹ã¦POSTã§ã€ãƒªã‚¯ã‚¨ã‚¹ãƒˆ/レスãƒãƒ³ã‚¹ã¨ã‚‚ã«JSONå½¢å¼ã§ã™ã€‚RESTã§ã¯ã‚ã‚Šã¾ã›ã‚“。** -APIã‚ーã¯ã€`i`ã¨ã„ã†ãƒ‘ラメータåã§ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å«ã‚ã¾ã™ã€‚ +アクセストークンã¯ã€`i`ã¨ã„ã†ãƒ‘ラメータåã§ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å«ã‚ã¾ã™ã€‚ * [APIリファレンス](/api-doc) * [ストリーミングAPI](./stream) diff --git a/src/models/entities/access-token.ts b/src/models/entities/access-token.ts index 137bf1444d487e46a9419d2201eae7c52f59cdb7..5f41b3c1fcce2cf260a9e57cc86fffe3b4361fee 100644 --- a/src/models/entities/access-token.ts +++ b/src/models/entities/access-token.ts @@ -13,12 +13,26 @@ export class AccessToken { }) public createdAt: Date; + @Column('timestamp with time zone', { + nullable: true, + default: null, + }) + public lastUsedAt: Date | null; + @Index() @Column('varchar', { length: 128 }) public token: string; + @Index() + @Column('varchar', { + length: 128, + nullable: true, + default: null + }) + public session: string | null; + @Index() @Column('varchar', { length: 128 @@ -35,12 +49,48 @@ export class AccessToken { @JoinColumn() public user: User | null; - @Column(id()) - public appId: App['id']; + @Column({ + ...id(), + nullable: true, + default: null + }) + public appId: App['id'] | null; @ManyToOne(type => App, { onDelete: 'CASCADE' }) @JoinColumn() public app: App | null; + + @Column('varchar', { + length: 128, + nullable: true, + default: null + }) + public name: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + default: null + }) + public description: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + default: null + }) + public iconUrl: string | null; + + @Column('varchar', { + length: 64, array: true, + default: '{}' + }) + public permission: string[]; + + @Column('boolean', { + default: false + }) + public fetched: boolean; } diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts index 519ed773889b2d7f62e4d38447e93224a629a26e..32ad3b4019d5f203ecaf3d385ece732c2e6f02df 100644 --- a/src/server/api/authenticate.ts +++ b/src/server/api/authenticate.ts @@ -1,7 +1,11 @@ import isNativeToken from './common/is-native-token'; import { User } from '../../models/entities/user'; -import { App } from '../../models/entities/app'; import { Users, AccessTokens, Apps } from '../../models'; +import { ensure } from '../../prelude/ensure'; + +type App = { + permission: string[]; +}; export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => { if (token == null) { @@ -27,14 +31,26 @@ export default async (token: string): Promise<[User | null | undefined, App | nu throw new Error('invalid signature'); } - const app = await Apps - .findOne(accessToken.appId); + AccessTokens.update(accessToken.id, { + lastUsedAt: new Date(), + }); const user = await Users .findOne({ id: accessToken.userId // findOne(accessToken.userId) ã®ã‚ˆã†ã«æ›¸ã‹ãªã„ã®ã¯å¾Œæ–¹äº’æ›æ€§ã®ãŸã‚ }); - return [user, app]; + if (accessToken.appId) { + const app = await Apps + .findOne(accessToken.appId).then(ensure); + + return [user, { + permission: app.permission + }]; + } else { + return [user, { + permission: accessToken.permission + }]; + } } }; diff --git a/src/server/api/call.ts b/src/server/api/call.ts index 37bcf7ce163d659c3c3b64bda11cec7d6a13de1f..c75006ef31f1eff02810ffa83cf85327dcb64237 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -4,7 +4,10 @@ import { User } from '../../models/entities/user'; import endpoints from './endpoints'; import { ApiError } from './error'; import { apiLogger } from './logger'; -import { App } from '../../models/entities/app'; + +type App = { + permission: string[]; +}; const accessDenied = { message: 'Access denied.', @@ -73,7 +76,7 @@ export default async (endpoint: string, user: User | null | undefined, app: App // API invoking const before = performance.now(); - return await ep.exec(data, user, app, file).catch((e: Error) => { + return await ep.exec(data, user, isSecure, file).catch((e: Error) => { if (e instanceof ApiError) { throw e; } else { diff --git a/src/server/api/define.ts b/src/server/api/define.ts index 1fd4543bd0ee3c3d1bcdef5ca5b76dab18355294..3c9e6863b79565982d775be14e6564697eb213f6 100644 --- a/src/server/api/define.ts +++ b/src/server/api/define.ts @@ -2,7 +2,6 @@ import * as fs from 'fs'; import { ILocalUser } from '../../models/entities/user'; import { IEndpointMeta } from './endpoints'; import { ApiError } from './error'; -import { App } from '../../models/entities/app'; import { SchemaType } from '../../misc/schema'; // TODO: defaultãŒè¨å®šã•ã‚Œã¦ã„ã‚‹å ´åˆã¯ãã®åž‹ã‚‚考慮ã™ã‚‹ @@ -15,12 +14,12 @@ type Params<T extends IEndpointMeta> = { export type Response = Record<string, any> | void; type executor<T extends IEndpointMeta> = - (params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, app: App, file?: any, cleanup?: Function) => + (params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, isSecure: boolean, file?: any, cleanup?: Function) => Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>) - : (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, app: App, file?: any) => Promise<any> { - return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, app: App, file?: any) => { + : (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, isSecure: boolean, file?: any) => Promise<any> { + return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, isSecure: boolean, file?: any) => { function cleanup() { fs.unlink(file.path, () => {}); } @@ -37,7 +36,7 @@ export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>) return Promise.reject(pserr); } - return cb(ps, user, app, file, cleanup); + return cb(ps, user, isSecure, file, cleanup); }; } diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts index 2c8cdbe39633fabd49e96d358d7e586a2a411a16..c1bbb2c53e239cba4a94132eb63c78c6cc161803 100644 --- a/src/server/api/endpoints/app/show.ts +++ b/src/server/api/endpoints/app/show.ts @@ -28,9 +28,7 @@ export const meta = { } }; -export default define(meta, async (ps, user, app) => { - const isSecure = user != null && app == null; - +export default define(meta, async (ps, user, isSecure) => { // Lookup app const ap = await Apps.findOne(ps.appId); diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index 3c5c9825348c7b2cf1ff32fb3f42cd39e4629bad..5043fbb8e7cfe00b0ef47f55f22b81826d811ae2 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -78,7 +78,7 @@ export const meta = { } }; -export default define(meta, async (ps, user, app, file, cleanup) => { +export default define(meta, async (ps, user, isSecure, file, cleanup) => { // Get 'name' parameter let name = ps.name || file.originalname; if (name !== undefined && name !== null) { diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index d22de40c69623454a9101dc50b87c1110ff47634..3909d54663b8e85f0e011966d684273e1af9d058 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -19,9 +19,7 @@ export const meta = { }, }; -export default define(meta, async (ps, user, app) => { - const isSecure = user != null && app == null; - +export default define(meta, async (ps, user, isSecure) => { return await Users.pack(user, user, { detail: true, includeHasUnreadNotes: true, diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 5c4a9576e1e7cf15d1258b64225aa78e4e33c171..0a3ba824aee7c1da4a68270f9734090eb1a67b69 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -178,9 +178,7 @@ export const meta = { } }; -export default define(meta, async (ps, user, app) => { - const isSecure = user != null && app == null; - +export default define(meta, async (ps, user, isSecure) => { const updates = {} as Partial<User>; const profileUpdates = {} as Partial<UserProfile>; diff --git a/src/server/api/endpoints/miauth/gen-token.ts b/src/server/api/endpoints/miauth/gen-token.ts new file mode 100644 index 0000000000000000000000000000000000000000..599ecf477eab78619ee1df87bc07831438f95151 --- /dev/null +++ b/src/server/api/endpoints/miauth/gen-token.ts @@ -0,0 +1,54 @@ +import rndstr from 'rndstr'; +import $ from 'cafy'; +import define from '../../define'; +import { AccessTokens } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; + +export const meta = { + tags: ['auth'], + + requireCredential: true as const, + + secure: true, + + params: { + session: { + validator: $.str + }, + + name: { + validator: $.nullable.optional.str + }, + + description: { + validator: $.nullable.optional.str, + }, + + iconUrl: { + validator: $.nullable.optional.str, + }, + + permission: { + validator: $.arr($.str).unique(), + }, + }, +}; + +export default define(meta, async (ps, user) => { + // Generate access token + const accessToken = rndstr('a-zA-Z0-9', 32); + + // Insert access token doc + await AccessTokens.save({ + id: genId(), + createdAt: new Date(), + session: ps.session, + userId: user.id, + token: accessToken, + hash: accessToken, + name: ps.name, + description: ps.description, + iconUrl: ps.iconUrl, + permission: ps.permission, + }); +}); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index e983ad6fd62508faf431775e5f77221f82bbe4e1..cccf138add2ec47e51573d2427f6b36fc403f00e 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -209,7 +209,7 @@ export const meta = { } }; -export default define(meta, async (ps, user, app) => { +export default define(meta, async (ps, user) => { let visibleUsers: User[] = []; if (ps.visibleUserIds) { visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)))) @@ -281,7 +281,6 @@ export default define(meta, async (ps, user, app) => { reply, renote, cw: ps.cw, - app, viaMobile: ps.viaMobile, localOnly: ps.localOnly, visibility: ps.visibility, diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 258e632bd8725c3af02d6b7eec9569ac1eb664a5..49209ede43655f63533a8399578311359bd51939 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -15,7 +15,7 @@ import signin from './private/signin'; import discord from './service/discord'; import github from './service/github'; import twitter from './service/twitter'; -import { Instances } from '../../models'; +import { Instances, AccessTokens, Users } from '../../models'; // Init app const app = new Koa(); @@ -73,6 +73,28 @@ router.get('/v1/instance/peers', async ctx => { ctx.body = instances.map(instance => instance.host); }); +router.post('/miauth/:session/check', async ctx => { + const token = await AccessTokens.findOne({ + session: ctx.params.session + }); + + if (token && !token.fetched) { + AccessTokens.update(token.id, { + fetched: true + }); + + ctx.body = { + ok: true, + token: token.token, + user: await Users.pack(token.userId, null, { detail: true }) + }; + } else { + ctx.body = { + ok: false, + }; + } +}); + // Return 404 for unknown API router.all('*', async ctx => { ctx.status = 404; diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index 463ae0a60176fea881f23fe55dcbeb56fd4370b9..4b233b9a8b61a6b8259455e34271e66380ee6c90 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -7,10 +7,13 @@ import Channel from './channel'; import channels from './channels'; import { EventEmitter } from 'events'; import { User } from '../../../models/entities/user'; -import { App } from '../../../models/entities/app'; import { Users, Followings, Mutings } from '../../../models'; import { ApiError } from '../error'; +type App = { + permission: string[]; +}; + /** * Main stream connection */