summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAcid Chicken (硫酸鶏) <root@acid-chicken.com>2018-11-04 22:03:55 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2018-11-04 22:03:55 +0900
commit8646a9c49c708cd502904b450e16b405118312e3 (patch)
treed616b6bf70be75b4095f2c215fa5e291b548e756 /src
parentClean up (diff)
downloadmisskey-8646a9c49c708cd502904b450e16b405118312e3.tar.gz
misskey-8646a9c49c708cd502904b450e16b405118312e3.tar.bz2
misskey-8646a9c49c708cd502904b450e16b405118312e3.zip
Add GitHub auth (#3095)
Diffstat (limited to 'src')
-rw-r--r--src/client/app/common/views/components/github-setting.vue63
-rw-r--r--src/client/app/common/views/components/index.ts2
-rw-r--r--src/client/app/common/views/components/signin.vue1
-rw-r--r--src/client/app/desktop/views/components/settings.vue7
-rw-r--r--src/client/app/desktop/views/pages/user/user.github.vue27
-rw-r--r--src/client/app/desktop/views/pages/user/user.vue11
-rw-r--r--src/client/app/mobile/views/pages/settings.vue13
-rw-r--r--src/config/types.ts4
-rw-r--r--src/models/user.ts8
-rw-r--r--src/server/api/endpoints/meta.ts1
-rw-r--r--src/server/api/service/github.ts267
11 files changed, 396 insertions, 8 deletions
diff --git a/src/client/app/common/views/components/github-setting.vue b/src/client/app/common/views/components/github-setting.vue
new file mode 100644
index 0000000000..f79a700a92
--- /dev/null
+++ b/src/client/app/common/views/components/github-setting.vue
@@ -0,0 +1,63 @@
+<template>
+<div class="mk-github-setting">
+ <p>%i18n:@description%<a :href="`${docsUrl}/link-to-github`" target="_blank">%i18n:@detail%</a></p>
+ <p class="account" v-if="$store.state.i.github" :title="`GitHub ID: ${$store.state.i.github.id}`">%i18n:@connected-to%: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
+ <p>
+ <a :href="`${apiUrl}/connect/github`" target="_blank" @click.prevent="connect">{{ $store.state.i.github ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
+ <span v-if="$store.state.i.github"> or </span>
+ <a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github" @click.prevent="disconnect">%i18n:@disconnect%</a>
+ </p>
+ <p class="id" v-if="$store.state.i.github">GitHub ID: {{ $store.state.i.github.id }}</p>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { apiUrl, docsUrl } from '../../../config';
+
+export default Vue.extend({
+ data() {
+ return {
+ form: null,
+ apiUrl,
+ docsUrl
+ };
+ },
+ mounted() {
+ this.$watch('$store.state.i', () => {
+ if (this.$store.state.i.github && this.form)
+ this.form.close();
+ }, {
+ deep: true
+ });
+ },
+ methods: {
+ connect() {
+ this.form = window.open(apiUrl + '/connect/github',
+ 'github_connect_window',
+ 'height=570, width=520');
+ },
+
+ disconnect() {
+ window.open(apiUrl + '/disconnect/github',
+ 'github_disconnect_window',
+ 'height=570, width=520');
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-github-setting
+ .account
+ border solid 1px #e1e8ed
+ border-radius 4px
+ padding 16px
+
+ a
+ font-weight bold
+ color inherit
+
+ .id
+ color #8899a6
+</style>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index a3ddf10820..3b20d0753d 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -37,6 +37,7 @@ import messaging from './messaging.vue';
import messagingRoom from './messaging-room.vue';
import urlPreview from './url-preview.vue';
import twitterSetting from './twitter-setting.vue';
+import githubSetting from './github-setting.vue';
import fileTypeIcon from './file-type-icon.vue';
import Reversi from './games/reversi/reversi.vue';
import welcomeTimeline from './welcome-timeline.vue';
@@ -90,6 +91,7 @@ Vue.component('mk-messaging', messaging);
Vue.component('mk-messaging-room', messagingRoom);
Vue.component('mk-url-preview', urlPreview);
Vue.component('mk-twitter-setting', twitterSetting);
+Vue.component('mk-github-setting', githubSetting);
Vue.component('mk-file-type-icon', fileTypeIcon);
Vue.component('mk-reversi', Reversi);
Vue.component('mk-welcome-timeline', welcomeTimeline);
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 9224f82cb9..0b81daf176 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -13,6 +13,7 @@
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
+ <p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/github`">%i18n:@signin-with-github%</a></p>
</form>
</template>
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 983a0b9bc2..93bef0e618 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -23,6 +23,13 @@
<mk-twitter-setting/>
</section>
</ui-card>
+
+ <ui-card>
+ <div slot="title">%fa:B github% %i18n:@github%</div>
+ <section>
+ <mk-github-setting/>
+ </section>
+ </ui-card>
</div>
<ui-card class="theme" v-show="page == 'theme'">
diff --git a/src/client/app/desktop/views/pages/user/user.github.vue b/src/client/app/desktop/views/pages/user/user.github.vue
new file mode 100644
index 0000000000..abe99b8456
--- /dev/null
+++ b/src/client/app/desktop/views/pages/user/user.github.vue
@@ -0,0 +1,27 @@
+<template>
+<div class="aqooishiizumijmihokohinatamihoaz">
+ <span>%fa:B github%<a :href="`https://github.com/${user.github.login}`" target="_blank">@{{ user.github.login }}</a></span>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: ['user']
+});
+</script>
+
+<style lang="stylus" scoped>
+.aqooishiizumijmihokohinatamihoaz
+.adsvaidqfznoartcbplullnejvxjphcn
+ padding 32px
+ background #171515
+ border-radius 6px
+ color #fff
+
+ a
+ margin-left 8px
+ color #fff
+
+</style>
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index 0f58763f03..b137592c69 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -2,7 +2,7 @@
<mk-ui>
<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
<div class="is-suspended" v-if="user.isSuspended">%fa:exclamation-triangle% %i18n:@is-suspended%</div>
- <div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
+ <div class="is-remote" v-if="user.host">%fa:exclamation-triangle% %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
<main>
<div class="main">
<x-header :user="user"/>
@@ -12,14 +12,15 @@
<div class="side">
<div class="instance" v-if="!$store.getters.isSignedIn"><mk-instance/></div>
<x-profile :user="user"/>
- <x-twitter :user="user" v-if="user.host === null && user.twitter"/>
+ <x-twitter :user="user" v-if="!user.host && user.twitter"/>
+ <x-github :user="user" v-if="!user.host && user.github"/>
<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
<mk-activity :user="user"/>
<x-photos :user="user"/>
<x-friends :user="user"/>
<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
<div class="nav"><mk-nav/></div>
- <p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
+ <p v-if="!user.host">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
</div>
</main>
</div>
@@ -37,6 +38,7 @@ import XPhotos from './user.photos.vue';
import XFollowersYouKnow from './user.followers-you-know.vue';
import XFriends from './user.friends.vue';
import XTwitter from './user.twitter.vue';
+import XGithub from './user.github.vue'; // ?MEM: Don't fix the intentional typo. (XGitHub -> `<x-git-hub>`)
export default Vue.extend({
components: {
@@ -46,7 +48,8 @@ export default Vue.extend({
XPhotos,
XFollowersYouKnow,
XFriends,
- XTwitter
+ XTwitter,
+ XGithub // ?MEM: Don't fix the intentional typo. (see L41)
},
data() {
return {
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 4512e9b2c2..10d13423a1 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -125,6 +125,19 @@
</section>
</ui-card>
+ <ui-card>
+ <div slot="title">%fa:B github% %i18n:@github%</div>
+
+ <section>
+ <p class="account" v-if="$store.state.i.github"><a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
+ <p>
+ <a :href="`${apiUrl}/connect/github`" target="_blank">{{ $store.state.i.github ? '%i18n:@github-reconnect%' : '%i18n:@github-connect%' }}</a>
+ <span v-if="$store.state.i.github"> or </span>
+ <a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github">%i18n:@github-disconnect%</a>
+ </p>
+ </section>
+ </ui-card>
+
<mk-api-settings />
<ui-card>
diff --git a/src/config/types.ts b/src/config/types.ts
index ee919abdec..ab5ba4c04b 100644
--- a/src/config/types.ts
+++ b/src/config/types.ts
@@ -74,6 +74,10 @@ export type Source = {
consumer_key: string;
consumer_secret: string;
};
+ github?: {
+ client_id: string;
+ client_secret: string;
+ };
github_bot?: {
hook_secret: string;
username: string;
diff --git a/src/models/user.ts b/src/models/user.ts
index 1e5b6ad74e..43ca612b51 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -82,6 +82,11 @@ export interface ILocalUser extends IUserBase {
userId: string;
screenName: string;
};
+ github: {
+ accessToken: string;
+ id: string;
+ login: string;
+ };
line: {
userId: string;
};
@@ -280,6 +285,9 @@ export const pack = (
delete _user.twitter.accessToken;
delete _user.twitter.accessTokenSecret;
}
+ if (_user.github) {
+ delete _user.github.accessToken;
+ }
delete _user.line;
// Visible via only the official client
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 3d26003a1a..b3027cd5ef 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -73,6 +73,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
recaptcha: config.recaptcha ? true : false,
objectStorage: config.drive && config.drive.storage === 'minio',
twitter: config.twitter ? true : false,
+ github: config.github ? true : false,
serviceWorker: config.sw ? true : false,
userRecommendation: config.user_recommendation ? config.user_recommendation : {}
} : undefined
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts
index ac18cf90ae..3296f6fd69 100644
--- a/src/server/api/service/github.ts
+++ b/src/server/api/service/github.ts
@@ -1,11 +1,16 @@
import * as EventEmitter from 'events';
+import * as Koa from 'koa';
import * as Router from 'koa-router';
import * as request from 'request';
-const crypto = require('crypto');
-
-import User, { IUser } from '../../../models/user';
+import { OAuth2 } from 'oauth';
+import User, { IUser, pack, ILocalUser } from '../../../models/user';
import createNote from '../../../services/note/create';
import config from '../../../config';
+import { publishMainStream } from '../../../stream';
+import redis from '../../../db/redis';
+import uuid = require('uuid');
+import signin from '../common/signin';
+const crypto = require('crypto');
const handler = new EventEmitter();
@@ -28,10 +33,264 @@ const post = async (text: string, home = true) => {
createNote(bot, { text, visibility: home ? 'home' : 'public' });
};
+function getUserToken(ctx: Koa.Context) {
+ return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
+}
+
+function compareOrigin(ctx: Koa.Context) {
+ function normalizeUrl(url: string) {
+ return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
+ }
+
+ const referer = ctx.headers['referer'];
+
+ return (normalizeUrl(referer) == normalizeUrl(config.url));
+}
+
// Init router
const router = new Router();
-if (config.github_bot != null) {
+router.get('/disconnect/github', async ctx => {
+ if (!compareOrigin(ctx)) {
+ ctx.throw(400, 'invalid origin');
+ return;
+ }
+
+ const userToken = getUserToken(ctx);
+ if (!userToken) {
+ ctx.throw(400, 'signin required');
+ return;
+ }
+
+ const user = await User.findOneAndUpdate({
+ host: null,
+ 'token': userToken
+ }, {
+ $set: {
+ 'github': null
+ }
+ });
+
+ ctx.body = `GitHubの連携を解除しました :v:`;
+
+ // Publish i updated event
+ publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+ detail: true,
+ includeSecrets: true
+ }));
+});
+
+if (!config.github || !redis) {
+ router.get('/connect/github', ctx => {
+ ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)';
+ });
+
+ router.get('/signin/github', ctx => {
+ ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)';
+ });
+} else {
+ const oauth2 = new OAuth2(
+ config.github.client_id,
+ config.github.client_secret,
+ 'https://github.com/',
+ 'login/oauth/authorize',
+ 'login/oauth/access_token');
+
+ router.get('/connect/github', async ctx => {
+ if (!compareOrigin(ctx)) {
+ ctx.throw(400, 'invalid origin');
+ return;
+ }
+
+ const userToken = getUserToken(ctx);
+ if (!userToken) {
+ ctx.throw(400, 'signin required');
+ return;
+ }
+
+ const params = {
+ redirect_uri: `${config.url}:8089/api/gh/cb`,
+ scope: ['read:user'],
+ state: uuid()
+ };
+
+ redis.set(userToken, JSON.stringify(params));
+ ctx.redirect(oauth2.getAuthorizeUrl(params));
+ });
+
+ router.get('/signin/github', async ctx => {
+ const sessid = uuid();
+
+ const params = {
+ redirect_uri: `${config.url}:8089/api/gh/cb`,
+ scope: ['read:user'],
+ state: uuid()
+ };
+
+ const expires = 1000 * 60 * 60; // 1h
+ ctx.cookies.set('signin_with_github_session_id', sessid, {
+ path: '/',
+ domain: config.host,
+ secure: config.url.startsWith('https'),
+ httpOnly: true,
+ expires: new Date(Date.now() + expires),
+ maxAge: expires
+ });
+
+ redis.set(sessid, JSON.stringify(params));
+ ctx.redirect(oauth2.getAuthorizeUrl(params));
+ });
+
+ router.get('/gh/cb', async ctx => {
+ const userToken = getUserToken(ctx);
+
+ if (!userToken) {
+ const sessid = ctx.cookies.get('signin_with_github_session_id');
+
+ if (!sessid) {
+ ctx.throw(400, 'invalid session');
+ return;
+ }
+
+ const code = ctx.query.code;
+
+ if (!code) {
+ ctx.throw(400, 'invalid session');
+ return;
+ }
+
+ const { redirect_uri, state } = await new Promise<any>((res, rej) => {
+ redis.get(sessid, async (_, state) => {
+ res(JSON.parse(state));
+ });
+ });
+
+ if (ctx.query.state !== state) {
+ ctx.throw(400, 'invalid session');
+ return;
+ }
+
+ const { accessToken } = await new Promise<any>((res, rej) =>
+ oauth2.getOAuthAccessToken(
+ code,
+ { redirect_uri },
+ (err, accessToken, refresh, result) => {
+ if (err)
+ rej(err);
+ else if (result.error)
+ rej(result.error);
+ else
+ res({ accessToken });
+ }));
+
+ const { login, id } = await new Promise<any>((res, rej) =>
+ request({
+ url: 'https://api.github.com/user',
+ headers: {
+ 'Accept': 'application/vnd.github.v3+json',
+ 'Authorization': `bearer ${accessToken}`,
+ 'User-Agent': config.user_agent
+ }
+ }, (err, response, body) => {
+ if (err)
+ rej(err);
+ else
+ res(JSON.parse(body));
+ }));
+
+ if (!login || !id) {
+ ctx.throw(400, 'invalid session');
+ return;
+ }
+
+ const user = await User.findOne({
+ host: null,
+ 'github.id': id
+ }) as ILocalUser;
+
+ if (!user) {
+ ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
+ return;
+ }
+
+ signin(ctx, user, true);
+ } else {
+ const code = ctx.query.code;
+
+ if (!code) {
+ ctx.throw(400, 'invalid session');
+ return;
+ }
+
+ const { redirect_uri, state } = await new Promise<any>((res, rej) => {
+ redis.get(userToken, async (_, state) => {
+ res(JSON.parse(state));
+ });
+ });
+
+ if (ctx.query.state !== state) {
+ ctx.throw(400, 'invalid session');
+ return;
+ }
+
+ const { accessToken } = await new Promise<any>((res, rej) =>
+ oauth2.getOAuthAccessToken(
+ code,
+ { redirect_uri },
+ (err, accessToken, refresh, result) => {
+ if (err)
+ rej(err);
+ else if (result.error)
+ rej(result.error);
+ else
+ res({ accessToken });
+ }));
+
+ const { login, id } = await new Promise<any>((res, rej) =>
+ request({
+ url: 'https://api.github.com/user',
+ headers: {
+ 'Accept': 'application/vnd.github.v3+json',
+ 'Authorization': `bearer ${accessToken}`,
+ 'User-Agent': config.user_agent
+ }
+ }, (err, response, body) => {
+ if (err)
+ rej(err);
+ else
+ res(JSON.parse(body));
+ }));
+
+ if (!login || !id) {
+ ctx.throw(400, 'invalid session');
+ return;
+ }
+
+ const user = await User.findOneAndUpdate({
+ host: null,
+ token: userToken
+ }, {
+ $set: {
+ github: {
+ accessToken,
+ id,
+ login
+ }
+ }
+ });
+
+ ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
+
+ // Publish i updated event
+ publishMainStream(user._id, 'meUpdated', await pack(user, user, {
+ detail: true,
+ includeSecrets: true
+ }));
+ }
+ });
+}
+
+if (config.github_bot) {
const secret = config.github_bot.hook_secret;
router.post('/hooks/github', ctx => {