diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2020-03-28 19:52:41 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2020-03-28 19:52:41 +0900 |
| commit | f014a79f8dbb101a80d638d025a27bbb5ea02575 (patch) | |
| tree | cc8af5c152e6470cddf1f34d083e2e0e222e7610 /src/server/api | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.27.0 (diff) | |
| download | sharkey-f014a79f8dbb101a80d638d025a27bbb5ea02575.tar.gz sharkey-f014a79f8dbb101a80d638d025a27bbb5ea02575.tar.bz2 sharkey-f014a79f8dbb101a80d638d025a27bbb5ea02575.zip | |
Merge branch 'develop'
Diffstat (limited to 'src/server/api')
| -rw-r--r-- | src/server/api/authenticate.ts | 22 | ||||
| -rw-r--r-- | src/server/api/call.ts | 10 | ||||
| -rw-r--r-- | src/server/api/define.ts | 10 | ||||
| -rw-r--r-- | src/server/api/endpoints/app/show.ts | 4 | ||||
| -rw-r--r-- | src/server/api/endpoints/drive/files/create.ts | 2 | ||||
| -rw-r--r-- | src/server/api/endpoints/i.ts | 4 | ||||
| -rw-r--r-- | src/server/api/endpoints/i/apps.ts | 41 | ||||
| -rw-r--r-- | src/server/api/endpoints/i/revoke-token.ts | 24 | ||||
| -rw-r--r-- | src/server/api/endpoints/i/update.ts | 4 | ||||
| -rw-r--r-- | src/server/api/endpoints/miauth/gen-token.ts | 55 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/create.ts | 3 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/polls/vote.ts | 6 | ||||
| -rw-r--r-- | src/server/api/endpoints/notifications/create.ts | 37 | ||||
| -rw-r--r-- | src/server/api/endpoints/users/groups/invite.ts | 3 | ||||
| -rw-r--r-- | src/server/api/index.ts | 24 | ||||
| -rw-r--r-- | src/server/api/openapi/description.ts | 47 | ||||
| -rw-r--r-- | src/server/api/stream/index.ts | 10 |
17 files changed, 227 insertions, 79 deletions
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts index 519ed77388..c3e277e8de 100644 --- a/src/server/api/authenticate.ts +++ b/src/server/api/authenticate.ts @@ -1,9 +1,10 @@ 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'; +import { AccessToken } from '../../models/entities/access-token'; -export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => { +export default async (token: string): Promise<[User | null | undefined, AccessToken | null | undefined]> => { if (token == null) { return [null, null]; } @@ -27,14 +28,25 @@ 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, { + id: accessToken.id, + permission: app.permission + } as AccessToken]; + } else { + return [user, accessToken]; + } } }; diff --git a/src/server/api/call.ts b/src/server/api/call.ts index 37bcf7ce16..7911eb9b49 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -4,7 +4,7 @@ import { User } from '../../models/entities/user'; import endpoints from './endpoints'; import { ApiError } from './error'; import { apiLogger } from './logger'; -import { App } from '../../models/entities/app'; +import { AccessToken } from '../../models/entities/access-token'; const accessDenied = { message: 'Access denied.', @@ -12,8 +12,8 @@ const accessDenied = { id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e' }; -export default async (endpoint: string, user: User | null | undefined, app: App | null | undefined, data: any, file?: any) => { - const isSecure = user != null && app == null; +export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => { + const isSecure = user != null && token == null; const ep = endpoints.find(e => e.name === endpoint); @@ -51,7 +51,7 @@ export default async (endpoint: string, user: User | null | undefined, app: App throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); } - if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) { + if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { throw new ApiError({ message: 'Your app does not have the necessary permissions to use this endpoint.', code: 'PERMISSION_DENIED', @@ -73,7 +73,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, token, 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 1fd4543bd0..2ee0ba4868 100644 --- a/src/server/api/define.ts +++ b/src/server/api/define.ts @@ -2,8 +2,8 @@ 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'; +import { AccessToken } from '../../models/entities/access-token'; // TODO: defaultが設定されている場合はその型も考慮する type Params<T extends IEndpointMeta> = { @@ -15,12 +15,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, token: AccessToken, 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, token: AccessToken, file?: any) => Promise<any> { + return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken, file?: any) => { function cleanup() { fs.unlink(file.path, () => {}); } @@ -37,7 +37,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, token, file, cleanup); }; } diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts index 2c8cdbe396..e3f3a1eaa4 100644 --- a/src/server/api/endpoints/app/show.ts +++ b/src/server/api/endpoints/app/show.ts @@ -28,8 +28,8 @@ export const meta = { } }; -export default define(meta, async (ps, user, app) => { - const isSecure = user != null && app == null; +export default define(meta, async (ps, user, token) => { + const isSecure = token == null; // 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 3c5c982534..c0bb6bcc6e 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, _, 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 d22de40c69..02d59682b8 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -19,8 +19,8 @@ export const meta = { }, }; -export default define(meta, async (ps, user, app) => { - const isSecure = user != null && app == null; +export default define(meta, async (ps, user, token) => { + const isSecure = token == null; return await Users.pack(user, user, { detail: true, diff --git a/src/server/api/endpoints/i/apps.ts b/src/server/api/endpoints/i/apps.ts new file mode 100644 index 0000000000..38299cd0dc --- /dev/null +++ b/src/server/api/endpoints/i/apps.ts @@ -0,0 +1,41 @@ +import $ from 'cafy'; +import define from '../../define'; +import { AccessTokens } from '../../../../models'; + +export const meta = { + requireCredential: true as const, + + secure: true, + + params: { + sort: { + validator: $.optional.str.or([ + '+createdAt', + '-createdAt', + '+lastUsedAt', + '-lastUsedAt', + ]), + }, + } +}; + +export default define(meta, async (ps, user) => { + const query = AccessTokens.createQueryBuilder('token'); + + switch (ps.sort) { + case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break; + case '+lastUsedAt': query.andWhere('token.lastUsedAt IS NOT NULL').orderBy('token.lastUsedAt', 'DESC'); break; + case '-lastUsedAt': query.andWhere('token.lastUsedAt IS NOT NULL').orderBy('token.lastUsedAt', 'ASC'); break; + default: query.orderBy('token.id', 'ASC'); break; + } + + const tokens = await query.getMany(); + + return await Promise.all(tokens.map(token => ({ + id: token.id, + name: token.name, + createdAt: token.createdAt, + lastUsedAt: token.lastUsedAt, + }))); +}); diff --git a/src/server/api/endpoints/i/revoke-token.ts b/src/server/api/endpoints/i/revoke-token.ts new file mode 100644 index 0000000000..ce688c5755 --- /dev/null +++ b/src/server/api/endpoints/i/revoke-token.ts @@ -0,0 +1,24 @@ +import $ from 'cafy'; +import define from '../../define'; +import { AccessTokens } from '../../../../models'; +import { ID } from '../../../../misc/cafy-id'; + +export const meta = { + requireCredential: true as const, + + secure: true, + + params: { + tokenId: { + validator: $.type(ID) + } + } +}; + +export default define(meta, async (ps, user) => { + const token = await AccessTokens.findOne(ps.tokenId); + + if (token) { + AccessTokens.delete(token.id); + } +}); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 5c4a9576e1..c90f050251 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -178,8 +178,8 @@ export const meta = { } }; -export default define(meta, async (ps, user, app) => { - const isSecure = user != null && app == null; +export default define(meta, async (ps, user, token) => { + const isSecure = token == null; 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 0000000000..efa8680805 --- /dev/null +++ b/src/server/api/endpoints/miauth/gen-token.ts @@ -0,0 +1,55 @@ +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(), + lastUsedAt: 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 e983ad6fd6..cccf138add 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/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index 3c5492f8ee..45210b5a6e 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -132,7 +132,8 @@ export default define(meta, async (ps, user) => { }); // Notify - createNotification(note.userId, user.id, 'pollVote', { + createNotification(note.userId, 'pollVote', { + notifierId: user.id, noteId: note.id, choice: ps.choice }); @@ -143,7 +144,8 @@ export default define(meta, async (ps, user) => { userId: Not(user.id), }).then(watchers => { for (const watcher of watchers) { - createNotification(watcher.userId, user.id, 'pollVote', { + createNotification(watcher.userId, 'pollVote', { + notifierId: user.id, noteId: note.id, choice: ps.choice }); diff --git a/src/server/api/endpoints/notifications/create.ts b/src/server/api/endpoints/notifications/create.ts new file mode 100644 index 0000000000..fed422b645 --- /dev/null +++ b/src/server/api/endpoints/notifications/create.ts @@ -0,0 +1,37 @@ +import $ from 'cafy'; +import define from '../../define'; +import { createNotification } from '../../../../services/create-notification'; + +export const meta = { + tags: ['notifications'], + + requireCredential: true as const, + + kind: 'write:notifications', + + params: { + body: { + validator: $.str + }, + + header: { + validator: $.optional.nullable.str + }, + + icon: { + validator: $.optional.nullable.str + }, + }, + + errors: { + } +}; + +export default define(meta, async (ps, user, token) => { + createNotification(user.id, 'app', { + appAccessTokenId: token.id, + customBody: ps.body, + customHeader: ps.header, + customIcon: ps.icon, + }); +}); diff --git a/src/server/api/endpoints/users/groups/invite.ts b/src/server/api/endpoints/users/groups/invite.ts index da0fd1c2ca..a0f5091b07 100644 --- a/src/server/api/endpoints/users/groups/invite.ts +++ b/src/server/api/endpoints/users/groups/invite.ts @@ -104,7 +104,8 @@ export default define(meta, async (ps, me) => { } as UserGroupInvitation); // 通知を作成 - createNotification(user.id, me.id, 'groupInvited', { + createNotification(user.id, 'groupInvited', { + notifierId: me.id, userGroupInvitationId: invitation.id }); }); diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 258e632bd8..49209ede43 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/openapi/description.ts b/src/server/api/openapi/description.ts index a4b79d9502..4fff53ac90 100644 --- a/src/server/api/openapi/description.ts +++ b/src/server/api/openapi/description.ts @@ -42,52 +42,7 @@ export function getDescription(lang = 'ja-JP'): string { .join('\n'); const descriptions = { - 'ja-JP': `**Misskey is a decentralized microblogging platform.** - -# Usage -**APIはすべてPOSTでリクエスト/レスポンスともにJSON形式です。** -一部のAPIはリクエストに認証情報(APIキー)が必要です。リクエストの際に\`i\`というパラメータでAPIキーを添付してください。 - -## 自分のアカウントのAPIキーを取得する -「設定 > API」で、自分のAPIキーを取得できます。 - -> アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。 - -## アプリケーションとしてAPIキーを取得する -直接ユーザーのAPIキーをアプリケーションが扱うのはセキュリティ上のリスクがあるので、 -アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のAPIキーを発行します。 - -### 1.アプリケーションを登録する -まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。 -[デベロッパーセンター](/dev)にアクセスし、「アプリ > アプリ作成」からアプリを作成してください。 - -登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。 - -> アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。</p> - -### 2.ユーザーに認証させる -アプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。 - -認証セッションを開始するには、[${config.apiUrl}/auth/session/generate](#operation/auth/session/generate) へパラメータに\`appSecret\`としてシークレットキーを含めたリクエストを送信します。 -レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。 - -あなたのアプリがコールバックURLを設定している場合、 -ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに\`token\`という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。 - -あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。 - -### 3.アクセストークンを取得する -ユーザーが連携を許可したら、[${config.apiUrl}/auth/session/userkey](#operation/auth/session/userkey) へリクエストを送信します。 - -上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます! - -アクセストークンが取得できたら、*「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」*をAPIキーとして、APIにリクエストできます。 - -APIキーの生成方法を擬似コードで表すと次のようになります: -\`\`\` js -const i = sha256(userToken + secretKey); -\`\`\` - + 'ja-JP': ` # Permissions |Permisson (kind)|Description|Endpoints| |:--|:--|:--| diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index 463ae0a601..05594ac722 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -7,9 +7,9 @@ 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'; +import { AccessToken } from '../../../models/entities/access-token'; /** * Main stream connection @@ -18,7 +18,7 @@ export default class Connection { public user?: User; public following: User['id'][] = []; public muting: User['id'][] = []; - public app: App; + public token: AccessToken; private wsConnection: websocket.connection; public subscriber: EventEmitter; private channels: Channel[] = []; @@ -30,12 +30,12 @@ export default class Connection { wsConnection: websocket.connection, subscriber: EventEmitter, user: User | null | undefined, - app: App | null | undefined + token: AccessToken | null | undefined ) { this.wsConnection = wsConnection; this.subscriber = subscriber; if (user) this.user = user; - if (app) this.app = app; + if (token) this.token = token; this.wsConnection.on('message', this.onWsConnectionMessage); @@ -83,7 +83,7 @@ export default class Connection { const endpoint = payload.endpoint || payload.ep; // alias // 呼び出し - call(endpoint, user, this.app, payload.data).then(res => { + call(endpoint, user, this.token, payload.data).then(res => { this.sendMessageToWs(`api:${payload.id}`, { res }); }).catch((e: ApiError) => { this.sendMessageToWs(`api:${payload.id}`, { |