Implement MiAuth
This commit is contained in:
		
							parent
							
								
									608b8bb741
								
							
						
					
					
						commit
						6be127e18b
					
				
					 19 changed files with 330 additions and 48 deletions
				
			
		|  | @ -568,7 +568,11 @@ _permissions: | ||||||
| 
 | 
 | ||||||
| _auth: | _auth: | ||||||
|   shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?" |   shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?" | ||||||
|  |   shareAccessAsk: "アカウントへのアクセスを許可しますか?" | ||||||
|   permissionAsk: "このアプリは次の権限を要求しています" |   permissionAsk: "このアプリは次の権限を要求しています" | ||||||
|  |   pleaseGoBack: "アプリケーションに戻ってやっていってください" | ||||||
|  |   callback: "アプリケーションに戻っています" | ||||||
|  |   denied: "アクセスを拒否しました" | ||||||
| 
 | 
 | ||||||
| _antennaSources: | _antennaSources: | ||||||
|   all: "全てのノート" |   all: "全てのノート" | ||||||
|  |  | ||||||
							
								
								
									
										36
									
								
								migration/1585361548360-miauth.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								migration/1585361548360-miauth.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -12,20 +12,18 @@ | ||||||
| 		@accepted="accepted" | 		@accepted="accepted" | ||||||
| 	/> | 	/> | ||||||
| 	<div class="denied _panel" v-if="state == 'denied'"> | 	<div class="denied _panel" v-if="state == 'denied'"> | ||||||
| 		<h1>{{ $t('denied') }}</h1> | 		<h1>{{ $t('_auth.denied') }}</h1> | ||||||
| 		<p>{{ $t('denied-paragraph') }}</p> |  | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="accepted _panel" v-if="state == 'accepted'"> | 	<div class="accepted _panel" v-if="state == 'accepted'"> | ||||||
| 		<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1> | 		<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('_auth.callback') }}<mk-ellipsis/></p> | ||||||
| 		<p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p> | 		<p v-if="!session.app.callbackUrl">{{ $t('_auth.pleaseGoBack') }}</p> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="error _panel" v-if="state == 'fetch-session-error'"> | 	<div class="error _panel" v-if="state == 'fetch-session-error'"> | ||||||
| 		<p>{{ $t('error') }}</p> | 		<p>{{ $t('error') }}</p> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| <div class="signin" v-else> | <div class="signin" v-else> | ||||||
| 	<h1>{{ $t('sign-in') }}</h1> |  | ||||||
| 	<mk-signin @login="onLogin"/> | 	<mk-signin @login="onLogin"/> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -118,6 +118,10 @@ export default Vue.extend({ | ||||||
| 		margin-bottom: 0; | 		margin-bottom: 0; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	::v-deep a { | ||||||
|  | 		color: var(--link); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	::v-deep h2 { | 	::v-deep h2 { | ||||||
| 		font-size: 1.25em; | 		font-size: 1.25em; | ||||||
| 		padding: 0 0 0.5em 0; | 		padding: 0 0 0.5em 0; | ||||||
|  |  | ||||||
							
								
								
									
										99
									
								
								src/client/pages/miauth.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/client/pages/miauth.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -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> | ||||||
|  | @ -58,6 +58,7 @@ export const router = new VueRouter({ | ||||||
| 		{ path: '/notes/:note', name: 'note', component: page('note') }, | 		{ path: '/notes/:note', name: 'note', component: page('note') }, | ||||||
| 		{ path: '/tags/:tag', component: page('tag') }, | 		{ path: '/tags/:tag', component: page('tag') }, | ||||||
| 		{ path: '/auth/:token', component: page('auth') }, | 		{ path: '/auth/:token', component: page('auth') }, | ||||||
|  | 		{ path: '/miauth/:session', component: page('miauth') }, | ||||||
| 		{ path: '/authorize-follow', component: page('follow') }, | 		{ path: '/authorize-follow', component: page('follow') }, | ||||||
| 		{ path: '/share', component: page('share') }, | 		{ path: '/share', component: page('share') }, | ||||||
| 		{ path: '*', component: page('not-found') } | 		{ path: '*', component: page('not-found') } | ||||||
|  |  | ||||||
|  | @ -3,25 +3,25 @@ | ||||||
| MisskeyAPIを使ってMisskeyクライアント、Misskey連携Webサービス、Bot等(以下「アプリケーション」と呼びます)を開発できます。 | 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の使い方」へ進む](#APIの使い方) | ||||||
| 
 | 
 | ||||||
| ### アプリケーションとしてAPIキーを取得する | ### アプリケーションとしてアクセストークンを取得する | ||||||
| アプリケーションを使ってもらうには、ユーザーのAPIキーを以下の手順で取得する必要があります。 | アプリケーションを使ってもらうには、ユーザーのアクセストークンを以下の手順で取得する必要があります。 | ||||||
| 
 | 
 | ||||||
| #### Step 1 | #### Step 1 | ||||||
| 
 | 
 | ||||||
|  | @ -38,23 +38,23 @@ UUIDを生成する。以後これをセッションIDと呼びます。 | ||||||
| * `callback` ... 認証が終わった後にリダイレクトするURL | * `callback` ... 認証が終わった後にリダイレクトするURL | ||||||
| 	* > 例: `https://missdeck.example.com/callback` | 	* > 例: `https://missdeck.example.com/callback` | ||||||
| 	* リダイレクト時には、`session`というクエリパラメータでセッションIDが付きます | 	* リダイレクト時には、`session`というクエリパラメータでセッションIDが付きます | ||||||
| * `permissions` ... アプリケーションが要求する権限 | * `permission` ... アプリケーションが要求する権限 | ||||||
| 	* > 例: `write:notes,write:following,read:drive` | 	* > 例: `write:notes,write:following,read:drive` | ||||||
| 	* 要求する権限を`,`で区切って列挙します | 	* 要求する権限を`,`で区切って列挙します | ||||||
| 	* どのような権限があるかは[APIリファレンス](/api-doc)で確認できます | 	* どのような権限があるかは[APIリファレンス](/api-doc)で確認できます | ||||||
| 
 | 
 | ||||||
| #### Step 3 | #### Step 3 | ||||||
| ユーザーが連携を許可した後、`{_URL_}/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてAPIキーを含むJSONが返ります。 | ユーザーが連携を許可した後、`{_URL_}/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。 | ||||||
| 
 | 
 | ||||||
| レスポンスに含まれるプロパティ: | レスポンスに含まれるプロパティ: | ||||||
| * `token` ... ユーザーのAPIキー | * `token` ... ユーザーのアクセストークン | ||||||
| * `user` ... ユーザーの情報 | * `user` ... ユーザーの情報 | ||||||
| 
 | 
 | ||||||
| [「APIの使い方」へ進む](#APIの使い方) | [「APIの使い方」へ進む](#APIの使い方) | ||||||
| 
 | 
 | ||||||
| ## APIの使い方 | ## APIの使い方 | ||||||
| **APIはすべてPOSTで、リクエスト/レスポンスともにJSON形式です。RESTではありません。** | **APIはすべてPOSTで、リクエスト/レスポンスともにJSON形式です。RESTではありません。** | ||||||
| APIキーは、`i`というパラメータ名でリクエストに含めます。 | アクセストークンは、`i`というパラメータ名でリクエストに含めます。 | ||||||
| 
 | 
 | ||||||
| * [APIリファレンス](/api-doc) | * [APIリファレンス](/api-doc) | ||||||
| * [ストリーミングAPI](./stream) | * [ストリーミングAPI](./stream) | ||||||
|  |  | ||||||
|  | @ -13,12 +13,26 @@ export class AccessToken { | ||||||
| 	}) | 	}) | ||||||
| 	public createdAt: Date; | 	public createdAt: Date; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('timestamp with time zone', { | ||||||
|  | 		nullable: true, | ||||||
|  | 		default: null, | ||||||
|  | 	}) | ||||||
|  | 	public lastUsedAt: Date | null; | ||||||
|  | 
 | ||||||
| 	@Index() | 	@Index() | ||||||
| 	@Column('varchar', { | 	@Column('varchar', { | ||||||
| 		length: 128 | 		length: 128 | ||||||
| 	}) | 	}) | ||||||
| 	public token: string; | 	public token: string; | ||||||
| 
 | 
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 128, | ||||||
|  | 		nullable: true, | ||||||
|  | 		default: null | ||||||
|  | 	}) | ||||||
|  | 	public session: string | null; | ||||||
|  | 
 | ||||||
| 	@Index() | 	@Index() | ||||||
| 	@Column('varchar', { | 	@Column('varchar', { | ||||||
| 		length: 128 | 		length: 128 | ||||||
|  | @ -35,12 +49,48 @@ export class AccessToken { | ||||||
| 	@JoinColumn() | 	@JoinColumn() | ||||||
| 	public user: User | null; | 	public user: User | null; | ||||||
| 
 | 
 | ||||||
| 	@Column(id()) | 	@Column({ | ||||||
| 	public appId: App['id']; | 		...id(), | ||||||
|  | 		nullable: true, | ||||||
|  | 		default: null | ||||||
|  | 	}) | ||||||
|  | 	public appId: App['id'] | null; | ||||||
| 
 | 
 | ||||||
| 	@ManyToOne(type => App, { | 	@ManyToOne(type => App, { | ||||||
| 		onDelete: 'CASCADE' | 		onDelete: 'CASCADE' | ||||||
| 	}) | 	}) | ||||||
| 	@JoinColumn() | 	@JoinColumn() | ||||||
| 	public app: App | null; | 	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; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,11 @@ | ||||||
| import isNativeToken from './common/is-native-token'; | import isNativeToken from './common/is-native-token'; | ||||||
| import { User } from '../../models/entities/user'; | import { User } from '../../models/entities/user'; | ||||||
| import { App } from '../../models/entities/app'; |  | ||||||
| import { Users, AccessTokens, Apps } from '../../models'; | 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]> => { | export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => { | ||||||
| 	if (token == null) { | 	if (token == null) { | ||||||
|  | @ -27,14 +31,26 @@ export default async (token: string): Promise<[User | null | undefined, App | nu | ||||||
| 			throw new Error('invalid signature'); | 			throw new Error('invalid signature'); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		const app = await Apps | 		AccessTokens.update(accessToken.id, { | ||||||
| 			.findOne(accessToken.appId); | 			lastUsedAt: new Date(), | ||||||
|  | 		}); | ||||||
| 
 | 
 | ||||||
| 		const user = await Users | 		const user = await Users | ||||||
| 			.findOne({ | 			.findOne({ | ||||||
| 				id: accessToken.userId // findOne(accessToken.userId) のように書かないのは後方互換性のため
 | 				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 | ||||||
|  | 			}]; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -4,7 +4,10 @@ import { User } from '../../models/entities/user'; | ||||||
| import endpoints from './endpoints'; | import endpoints from './endpoints'; | ||||||
| import { ApiError } from './error'; | import { ApiError } from './error'; | ||||||
| import { apiLogger } from './logger'; | import { apiLogger } from './logger'; | ||||||
| import { App } from '../../models/entities/app'; | 
 | ||||||
|  | type App = { | ||||||
|  | 	permission: string[]; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| const accessDenied = { | const accessDenied = { | ||||||
| 	message: 'Access denied.', | 	message: 'Access denied.', | ||||||
|  | @ -73,7 +76,7 @@ export default async (endpoint: string, user: User | null | undefined, app: App | ||||||
| 
 | 
 | ||||||
| 	// API invoking
 | 	// API invoking
 | ||||||
| 	const before = performance.now(); | 	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) { | 		if (e instanceof ApiError) { | ||||||
| 			throw e; | 			throw e; | ||||||
| 		} else { | 		} else { | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ import * as fs from 'fs'; | ||||||
| import { ILocalUser } from '../../models/entities/user'; | import { ILocalUser } from '../../models/entities/user'; | ||||||
| import { IEndpointMeta } from './endpoints'; | import { IEndpointMeta } from './endpoints'; | ||||||
| import { ApiError } from './error'; | import { ApiError } from './error'; | ||||||
| import { App } from '../../models/entities/app'; |  | ||||||
| import { SchemaType } from '../../misc/schema'; | import { SchemaType } from '../../misc/schema'; | ||||||
| 
 | 
 | ||||||
| // TODO: defaultが設定されている場合はその型も考慮する
 | // TODO: defaultが設定されている場合はその型も考慮する
 | ||||||
|  | @ -15,12 +14,12 @@ type Params<T extends IEndpointMeta> = { | ||||||
| export type Response = Record<string, any> | void; | export type Response = Record<string, any> | void; | ||||||
| 
 | 
 | ||||||
| type executor<T extends IEndpointMeta> = | 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']>>>; | 		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; | ||||||
| 
 | 
 | ||||||
| export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>) | 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> { | 		: (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, app: App, file?: any) => { | 	return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, isSecure: boolean, file?: any) => { | ||||||
| 		function cleanup() { | 		function cleanup() { | ||||||
| 			fs.unlink(file.path, () => {}); | 			fs.unlink(file.path, () => {}); | ||||||
| 		} | 		} | ||||||
|  | @ -37,7 +36,7 @@ export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>) | ||||||
| 			return Promise.reject(pserr); | 			return Promise.reject(pserr); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return cb(ps, user, app, file, cleanup); | 		return cb(ps, user, isSecure, file, cleanup); | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,9 +28,7 @@ export const meta = { | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user, app) => { | export default define(meta, async (ps, user, isSecure) => { | ||||||
| 	const isSecure = user != null && app == null; |  | ||||||
| 
 |  | ||||||
| 	// Lookup app
 | 	// Lookup app
 | ||||||
| 	const ap = await Apps.findOne(ps.appId); | 	const ap = await Apps.findOne(ps.appId); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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
 | 	// Get 'name' parameter
 | ||||||
| 	let name = ps.name || file.originalname; | 	let name = ps.name || file.originalname; | ||||||
| 	if (name !== undefined && name !== null) { | 	if (name !== undefined && name !== null) { | ||||||
|  |  | ||||||
|  | @ -19,9 +19,7 @@ export const meta = { | ||||||
| 	}, | 	}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user, app) => { | export default define(meta, async (ps, user, isSecure) => { | ||||||
| 	const isSecure = user != null && app == null; |  | ||||||
| 
 |  | ||||||
| 	return await Users.pack(user, user, { | 	return await Users.pack(user, user, { | ||||||
| 		detail: true, | 		detail: true, | ||||||
| 		includeHasUnreadNotes: true, | 		includeHasUnreadNotes: true, | ||||||
|  |  | ||||||
|  | @ -178,9 +178,7 @@ export const meta = { | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user, app) => { | export default define(meta, async (ps, user, isSecure) => { | ||||||
| 	const isSecure = user != null && app == null; |  | ||||||
| 
 |  | ||||||
| 	const updates = {} as Partial<User>; | 	const updates = {} as Partial<User>; | ||||||
| 	const profileUpdates = {} as Partial<UserProfile>; | 	const profileUpdates = {} as Partial<UserProfile>; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										54
									
								
								src/server/api/endpoints/miauth/gen-token.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/server/api/endpoints/miauth/gen-token.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | @ -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[] = []; | 	let visibleUsers: User[] = []; | ||||||
| 	if (ps.visibleUserIds) { | 	if (ps.visibleUserIds) { | ||||||
| 		visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)))) | 		visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)))) | ||||||
|  | @ -281,7 +281,6 @@ export default define(meta, async (ps, user, app) => { | ||||||
| 		reply, | 		reply, | ||||||
| 		renote, | 		renote, | ||||||
| 		cw: ps.cw, | 		cw: ps.cw, | ||||||
| 		app, |  | ||||||
| 		viaMobile: ps.viaMobile, | 		viaMobile: ps.viaMobile, | ||||||
| 		localOnly: ps.localOnly, | 		localOnly: ps.localOnly, | ||||||
| 		visibility: ps.visibility, | 		visibility: ps.visibility, | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ import signin from './private/signin'; | ||||||
| import discord from './service/discord'; | import discord from './service/discord'; | ||||||
| import github from './service/github'; | import github from './service/github'; | ||||||
| import twitter from './service/twitter'; | import twitter from './service/twitter'; | ||||||
| import { Instances } from '../../models'; | import { Instances, AccessTokens, Users } from '../../models'; | ||||||
| 
 | 
 | ||||||
| // Init app
 | // Init app
 | ||||||
| const app = new Koa(); | const app = new Koa(); | ||||||
|  | @ -73,6 +73,28 @@ router.get('/v1/instance/peers', async ctx => { | ||||||
| 	ctx.body = instances.map(instance => instance.host); | 	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
 | // Return 404 for unknown API
 | ||||||
| router.all('*', async ctx => { | router.all('*', async ctx => { | ||||||
| 	ctx.status = 404; | 	ctx.status = 404; | ||||||
|  |  | ||||||
|  | @ -7,10 +7,13 @@ import Channel from './channel'; | ||||||
| import channels from './channels'; | import channels from './channels'; | ||||||
| import { EventEmitter } from 'events'; | import { EventEmitter } from 'events'; | ||||||
| import { User } from '../../../models/entities/user'; | import { User } from '../../../models/entities/user'; | ||||||
| import { App } from '../../../models/entities/app'; |  | ||||||
| import { Users, Followings, Mutings } from '../../../models'; | import { Users, Followings, Mutings } from '../../../models'; | ||||||
| import { ApiError } from '../error'; | import { ApiError } from '../error'; | ||||||
| 
 | 
 | ||||||
|  | type App = { | ||||||
|  | 	permission: string[]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| /** | /** | ||||||
|  * Main stream connection |  * Main stream connection | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue