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: | ||||
|   shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?" | ||||
|   shareAccessAsk: "アカウントへのアクセスを許可しますか?" | ||||
|   permissionAsk: "このアプリは次の権限を要求しています" | ||||
|   pleaseGoBack: "アプリケーションに戻ってやっていってください" | ||||
|   callback: "アプリケーションに戻っています" | ||||
|   denied: "アクセスを拒否しました" | ||||
| 
 | ||||
| _antennaSources: | ||||
|   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" | ||||
| 	/> | ||||
| 	<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> | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
							
								
								
									
										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: '/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') } | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| 			}]; | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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); | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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>; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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[] = []; | ||||
| 	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, | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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 | ||||
|  */ | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue