Merge branch 'master' of github.com:syuilo/misskey into swagger

This commit is contained in:
Tosuke 2017-01-06 14:39:24 +09:00
commit 0420fee5d2
26 changed files with 147 additions and 193 deletions

View File

@ -1,6 +1,9 @@
language: node_js language: node_js
node_js: node_js:
- "7.3.0" - "7.3.0"
services:
- mongodb
- redis-server
before_script: before_script:
- "mkdir -p ./.config && cp ./.ci-files/config.yml ./.config" - "mkdir -p ./.config && cp ./.ci-files/config.yml ./.config"
env: env:

View File

@ -1,2 +1,11 @@
# 1 # Server
## 1
First version
# API
## 2
* パラメータ: _userkey --> i
* トークンは、アクセストークン + アプリのシークレットキーをsha512したものに
## 1
First version First version

View File

@ -70,5 +70,7 @@ block content
| 次に、<code>#{api_url}/auth/session/userkey</code>へ<code>app_secret</code>としてApp Secretを、<code>token</code>としてセッションのトークンをパラメータとして付与したリクエストを送信してください。 | 次に、<code>#{api_url}/auth/session/userkey</code>へ<code>app_secret</code>としてApp Secretを、<code>token</code>としてセッションのトークンをパラメータとして付与したリクエストを送信してください。
br br
| 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます! | 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
p
| 以降アクセストークンは、<strong>ユーザーのアクセストークン+アプリのシークレットキーをsha512したもの</strong>として扱います。
p アクセストークンを取得できたら、あとは簡単です。REST APIなら、リクエストにアクセストークンを<code>_userkey</code>(「自分のアクセストークンを取得したい場合」の方法で取得したアクセストークンの場合は<code>i</code>)としてパラメータに含めるだけです。 p アクセストークンを取得できたら、あとは簡単です。REST APIなら、リクエストにアクセストークンを<code>i</code>としてパラメータに含めるだけです。

View File

@ -149,6 +149,7 @@ const aliasifyConfig = {
'chart.js': './node_modules/chart.js/src/chart.js', 'chart.js': './node_modules/chart.js/src/chart.js',
'textarea-caret-position': './node_modules/textarea-caret/index.js', 'textarea-caret-position': './node_modules/textarea-caret/index.js',
'misskey-text': './src/common/text/index.js', 'misskey-text': './src/common/text/index.js',
'nyaize': './node_modules/nyaize/built/index.js',
'strength.js': './node_modules/syuilo-password-strength/strength.js', 'strength.js': './node_modules/syuilo-password-strength/strength.js',
'cropper': './node_modules/cropperjs/dist/cropper.js', 'cropper': './node_modules/cropperjs/dist/cropper.js',
'Sortable': './node_modules/sortablejs/Sortable.js', 'Sortable': './node_modules/sortablejs/Sortable.js',

View File

@ -25,7 +25,7 @@
"@types/browserify": "12.0.30", "@types/browserify": "12.0.30",
"@types/chalk": "0.4.31", "@types/chalk": "0.4.31",
"@types/compression": "0.0.33", "@types/compression": "0.0.33",
"@types/cors": "0.0.33", "@types/cors": "2.8.0",
"@types/elasticsearch": "5.0.9", "@types/elasticsearch": "5.0.9",
"@types/escape-html": "0.0.19", "@types/escape-html": "0.0.19",
"@types/event-stream": "3.3.30", "@types/event-stream": "3.3.30",
@ -63,13 +63,14 @@
"babel-preset-stage-3": "6.17.0", "babel-preset-stage-3": "6.17.0",
"bcrypt": "1.0.2", "bcrypt": "1.0.2",
"body-parser": "1.15.2", "body-parser": "1.15.2",
"browserify": "13.1.1", "browserify": "13.3.0",
"browserify-livescript": "0.2.3", "browserify-livescript": "0.2.3",
"chalk": "1.1.3", "chalk": "1.1.3",
"chart.js": "2.4.0", "chart.js": "2.4.0",
"compression": "1.6.2", "compression": "1.6.2",
"cors": "2.8.1", "cors": "2.8.1",
"cropperjs": "1.0.0-beta", "cropperjs": "1.0.0-beta",
"crypto": "0.0.3",
"deepcopy": "0.6.3", "deepcopy": "0.6.3",
"del": "2.2.2", "del": "2.2.2",
"elasticsearch": "12.1.3", "elasticsearch": "12.1.3",
@ -99,10 +100,11 @@
"livescript": "1.5.0", "livescript": "1.5.0",
"mime-types": "2.1.13", "mime-types": "2.1.13",
"mocha": "3.2.0", "mocha": "3.2.0",
"mongodb": "2.2.16", "mongodb": "2.2.19",
"ms": "0.7.2", "ms": "0.7.2",
"multer": "1.2.1", "multer": "1.2.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"nyaize": "0.0.2",
"page": "1.7.1", "page": "1.7.1",
"prominence": "0.2.0", "prominence": "0.2.0",
"pug": "2.0.0-beta6", "pug": "2.0.0-beta6",

View File

@ -1,7 +1,8 @@
import * as express from 'express'; import * as express from 'express';
import App from './models/app'; import App from './models/app';
import User from './models/user'; import User from './models/user';
import Userkey from './models/userkey'; import AccessToken from './models/access-token';
import isNativeToken from './common/is-native-token';
export interface IAuthContext { export interface IAuthContext {
/** /**
@ -20,10 +21,14 @@ export interface IAuthContext {
isSecure: boolean; isSecure: boolean;
} }
export default (req: express.Request) => export default (req: express.Request) => new Promise<IAuthContext>(async (resolve, reject) => {
new Promise<IAuthContext>(async (resolve, reject) => {
const token = req.body['i']; const token = req.body['i'];
if (token) {
if (token == null) {
return resolve({ app: null, user: null, isSecure: false });
}
if (isNativeToken(token)) {
const user = await User const user = await User
.findOne({ token: token }); .findOne({ token: token });
@ -36,26 +41,21 @@ export default (req: express.Request) =>
user: user, user: user,
isSecure: true isSecure: true
}); });
} } else {
const accessToken = await AccessToken.findOne({
const userkey = req.headers['userkey'] || req.body['_userkey']; hash: token
if (userkey) {
const userkeyDoc = await Userkey.findOne({
key: userkey
}); });
if (userkeyDoc === null) { if (accessToken === null) {
return reject('invalid userkey'); return reject('invalid signature');
} }
const app = await App const app = await App
.findOne({ _id: userkeyDoc.app_id }); .findOne({ _id: accessToken.app_id });
const user = await User const user = await User
.findOne({ _id: userkeyDoc.user_id }); .findOne({ _id: accessToken.user_id });
return resolve({ app: app, user: user, isSecure: false }); return resolve({ app: app, user: user, isSecure: false });
} }
return resolve({ app: null, user: null, isSecure: false });
}); });

View File

@ -0,0 +1 @@
export default (token: string) => token[0] == '!';

View File

@ -4,8 +4,10 @@
* Module dependencies * Module dependencies
*/ */
import rndstr from 'rndstr'; import rndstr from 'rndstr';
const crypto = require('crypto');
import App from '../../models/app';
import AuthSess from '../../models/auth-session'; import AuthSess from '../../models/auth-session';
import Userkey from '../../models/userkey'; import AccessToken from '../../models/access-token';
/** /**
* @swagger * @swagger
@ -41,35 +43,46 @@ module.exports = (params, user) =>
new Promise(async (res, rej) => new Promise(async (res, rej) =>
{ {
// Get 'token' parameter // Get 'token' parameter
const token = params.token; const sesstoken = params.token;
if (token == null) { if (sesstoken == null) {
return rej('token is required'); return rej('token is required');
} }
// Fetch token // Fetch token
const session = await AuthSess const session = await AuthSess
.findOne({ token: token }); .findOne({ token: sesstoken });
if (session === null) { if (session === null) {
return rej('session not found'); return rej('session not found');
} }
// Generate userkey // Generate access token
const key = rndstr('a-zA-Z0-9', 32); const token = rndstr('a-zA-Z0-9', 32);
// Fetch exist userkey // Fetch exist access token
const exist = await Userkey.findOne({ const exist = await AccessToken.findOne({
app_id: session.app_id, app_id: session.app_id,
user_id: user._id, user_id: user._id,
}); });
if (exist === null) { if (exist === null) {
// Insert userkey doc // Lookup app
await Userkey.insert({ const app = await App.findOne({
app_id: session.app_id
});
// Generate Hash
const sha512 = crypto.createHash('sha512');
sha512.update(token + app.secret);
const hash = sha512.digest('hex');
// Insert access token doc
await AccessToken.insert({
created_at: new Date(), created_at: new Date(),
app_id: session.app_id, app_id: session.app_id,
user_id: user._id, user_id: user._id,
key: key token: token,
hash: hash
}); });
} }

View File

@ -5,7 +5,7 @@
*/ */
import App from '../../../models/app'; import App from '../../../models/app';
import AuthSess from '../../../models/auth-session'; import AuthSess from '../../../models/auth-session';
import Userkey from '../../../models/userkey'; import AccessToken from '../../../models/access-token';
import serialize from '../../../serializers/user'; import serialize from '../../../serializers/user';
/** /**
@ -89,8 +89,8 @@ module.exports = (params) =>
return rej('this session is not allowed yet'); return rej('this session is not allowed yet');
} }
// Lookup userkey // Lookup access token
const userkey = await Userkey.findOne({ const accessToken = await AccessToken.findOne({
app_id: app._id, app_id: app._id,
user_id: session.user_id user_id: session.user_id
}); });
@ -102,7 +102,7 @@ module.exports = (params) =>
// Response // Response
res({ res({
userkey: userkey.key, access_token: accessToken.token,
user: await serialize(session.user_id, null, { user: await serialize(session.user_id, null, {
detail: true detail: true
}) })

View File

@ -19,9 +19,19 @@ module.exports = (params, me) =>
new Promise(async (res, rej) => new Promise(async (res, rej) =>
{ {
// Get 'user_id' parameter // Get 'user_id' parameter
const userId = params.user_id; let userId = params.user_id;
if (userId === undefined || userId === null) { if (userId === undefined || userId === null || userId === '') {
return rej('user_id is required'); userId = null;
}
// Get 'username' parameter
let username = params.username;
if (username === undefined || username === null || username === '') {
username = null;
}
if (userId === null && username === null) {
return rej('user_id or username is required');
} }
// Get 'with_replies' parameter // Get 'with_replies' parameter
@ -62,9 +72,9 @@ module.exports = (params, me) =>
} }
// Lookup user // Lookup user
const user = await User.findOne({ const user = userId !== null
_id: new mongo.ObjectID(userId) ? await User.findOne({ _id: new mongo.ObjectID(userId) })
}); : await User.findOne({ username_lower: username.toLowerCase() });
if (user === null) { if (user === null) {
return rej('user not found'); return rej('user not found');

View File

@ -0,0 +1,6 @@
const collection = global.db.collection('access_tokens');
collection.createIndex('token');
collection.createIndex('hash');
export default collection;

View File

@ -1,5 +0,0 @@
const collection = global.db.collection('userkeys');
collection.createIndex('key');
export default collection;

View File

@ -48,7 +48,7 @@ export default async (req: express.Request, res: express.Response) => {
const hash = bcrypt.hashSync(password, salt); const hash = bcrypt.hashSync(password, salt);
// Generate secret // Generate secret
const secret = rndstr('a-zA-Z0-9', 32); const secret = '!' + rndstr('a-zA-Z0-9', 32);
// Create account // Create account
const inserted = await User.insert({ const inserted = await User.insert({

View File

@ -7,7 +7,7 @@ import * as mongo from 'mongodb';
import deepcopy = require('deepcopy'); import deepcopy = require('deepcopy');
import App from '../models/app'; import App from '../models/app';
import User from '../models/user'; import User from '../models/user';
import Userkey from '../models/userkey'; import AccessToken from '../models/access-token';
/** /**
* Serialize an app * Serialize an app
@ -71,7 +71,7 @@ export default (
if (me) { if (me) {
// 既に連携しているか // 既に連携しているか
const exist = await Userkey.count({ const exist = await AccessToken.count({
app_id: _app.id, app_id: _app.id,
user_id: me, user_id: me,
}, { }, {

View File

@ -2,6 +2,8 @@ import * as http from 'http';
import * as websocket from 'websocket'; import * as websocket from 'websocket';
import * as redis from 'redis'; import * as redis from 'redis';
import User from './models/user'; import User from './models/user';
import AccessToken from './models/access-token';
import isNativeToken from './common/is-native-token';
import homeStream from './stream/home'; import homeStream from './stream/home';
import messagingStream from './stream/messaging'; import messagingStream from './stream/messaging';
@ -17,7 +19,13 @@ module.exports = (server: http.Server) => {
ws.on('request', async (request) => { ws.on('request', async (request) => {
const connection = request.accept(); const connection = request.accept();
const user = await authenticate(connection); const user = await authenticate(connection, request.resourceURL.query.i);
if (user == null) {
connection.send('authentication-failed');
connection.close();
return;
}
// Connect to Redis // Connect to Redis
const subscriber = redis.createClient( const subscriber = redis.createClient(
@ -41,29 +49,36 @@ module.exports = (server: http.Server) => {
}); });
}; };
function authenticate(connection: websocket.connection): Promise<any> { function authenticate(connection: websocket.connection, token: string): Promise<any> {
return new Promise((resolve, reject) => { return new Promise(async (resolve, reject) => {
// Listen first message if (isNativeToken(token)) {
connection.once('message', async (data) => {
const msg = JSON.parse(data.utf8Data);
// Fetch user // Fetch user
// SELECT _id // SELECT _id
const user = await User const user = await User
.findOne({ .findOne({
token: msg.i token: token
}, { }, {
_id: true _id: true
}); });
if (user === null) { resolve(user);
connection.close(); } else {
return; const accessToken = await AccessToken.findOne({
hash: token
});
if (accessToken == null) {
return reject('invalid signature');
} }
connection.send('authenticated'); // Fetch user
// SELECT _id
const user = await User
.findOne({ _id: accessToken.user_id }, {
_id: true
});
resolve(user); resolve(user);
}); }
}); });
} }

View File

@ -39,7 +39,7 @@ try {
checkForUpdate(); checkForUpdate();
// Get token from cookie // Get token from cookie
const i = (document.cookie.match(/i=(\w+)/) || [null, null])[1]; const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
// ユーザーをフェッチしてコールバックする // ユーザーをフェッチしてコールバックする
module.exports = callback => { module.exports = callback => {

View File

@ -3,7 +3,7 @@ module.exports = (date) ->
text = text =
date.get-full-year! + \年 + date.get-full-year! + \年 +
date.get-month! + \月 + date.get-month! + 1 + \月 +
date.get-date! + \日 + date.get-date! + \日 +
' ' + ' ' +
date.get-hours! + \時 + date.get-hours! + \時 +

View File

@ -9,7 +9,7 @@ class Connection
@event = riot.observable! @event = riot.observable!
@me = me @me = me
host = CONFIG.api.url.replace \http \ws host = CONFIG.api.url.replace \http \ws
@socket = new ReconnectingWebSocket "#{host}/messaging?otherparty=#{otherparty}" @socket = new ReconnectingWebSocket "#{host}/messaging?i=#{me.token}&otherparty=#{otherparty}"
@socket.add-event-listener \open @on-open @socket.add-event-listener \open @on-open
@socket.add-event-listener \message @on-message @socket.add-event-listener \message @on-message

View File

@ -9,13 +9,12 @@ module.exports = (me) ~>
state-ev = riot.observable! state-ev = riot.observable!
event = riot.observable! event = riot.observable!
socket = new ReconnectingWebSocket CONFIG.api.url.replace \http \ws host = CONFIG.api.url.replace \http \ws
socket = new ReconnectingWebSocket "#{host}?i=#{me.token}"
socket.onopen = ~> socket.onopen = ~>
state := \connected state := \connected
state-ev.trigger \connected state-ev.trigger \connected
socket.send JSON.stringify do
i: me.token
socket.onclose = ~> socket.onclose = ~>
state := \reconnecting state := \reconnecting

View File

@ -1,4 +1,5 @@
const riot = require('riot'); const riot = require('riot');
const nyaize = require('nyaize').default;
module.exports = function(tokens, shouldBreak, escape) { module.exports = function(tokens, shouldBreak, escape) {
if (shouldBreak == null) { if (shouldBreak == null) {
@ -34,10 +35,7 @@ module.exports = function(tokens, shouldBreak, escape) {
}).join(''); }).join('');
if (me && me.data && me.data.nya) { if (me && me.data && me.data.nya) {
text = text.replace(/な/g, 'にゃ') text = nyaize(text);
.replace(/ニャ/g, 'にゃ')
.replace(/にゃでにゃで/g, 'なでなで')
.replace(/ニャデニャデ/g, 'ナデナデ');
} }
return text; return text;

View File

@ -11,7 +11,7 @@ script.
@absolute = @absolute =
@time.get-full-year! + \年 + @time.get-full-year! + \年 +
@time.get-month! + \月 + @time.get-month! + 1 + \月 +
@time.get-date! + \日 + @time.get-date! + \日 +
' ' + ' ' +
@time.get-hours! + \時 + @time.get-hours! + \時 +

View File

@ -32,11 +32,6 @@ boot(me => {
// Register mixins // Register mixins
mixins(me); mixins(me);
// Debug
if (me != null && me.data.debug) {
riot.mount(document.body.appendChild(document.createElement('mk-log-window')));
}
// Start routing // Start routing
route(me); route(me);
}); });

View File

@ -99,5 +99,3 @@ require './tags/user-followers-window.tag'
require './tags/list-user.tag' require './tags/list-user.tag'
require './tags/ui-notification.tag' require './tags/ui-notification.tag'
require './tags/signin-history.tag' require './tags/signin-history.tag'
require './tags/log.tag'
require './tags/log-window.tag'

View File

@ -1,20 +0,0 @@
mk-log-window
mk-window@window(width={ '600px' }, height={ '400px' })
<yield to="header">
i.fa.fa-terminal
| Log
</yield>
<yield to="content">
mk-log
</yield>
style.
> mk-window
[data-yield='header']
> i
margin-right 4px
script.
@on \mount ~>
@refs.window.on \closed ~>
@unmount!

View File

@ -1,62 +0,0 @@
mk-log
header
button.follow(class={ following: following }, onclick={ follow }) Follow
div.logs@logs
code(each={ logs })
span.date { date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() }
span.message { message }
style.
display block
height 100%
color #fff
background #000
> header
height 32px
background #343a42
> button
line-height 32px
> .follow
position absolute
top 0
right 0
&.following
color #ff0
> .logs
height calc(100% - 32px)
overflow auto
> code
display block
padding 4px 8px
&:hover
background rgba(#fff, 0.15)
> .date
margin-right 8px
opacity 0.5
script.
@mixin \log
@following = true
@on \mount ~>
@log-event.on \log @on-log
@on \unmount ~>
@log-event.off \log @on-log
@follow = ~>
@following = true
@on-log = ~>
@update!
if @following
@refs.logs.scroll-top = @refs.logs.scroll-height

View File

@ -3,11 +3,10 @@ mk-user-preview
img.avatar(src={ user.avatar_url + '?thumbnail&size=64' }, alt='avatar') img.avatar(src={ user.avatar_url + '?thumbnail&size=64' }, alt='avatar')
div.main div.main
header header
div.left a.name(href={ CONFIG.url + '/' + user.username })
a.name(href={ CONFIG.url + '/' + user.username }) | { user.name }
| { user.name } span.username
span.username | @{ user.username }
| @{ user.username }
div.body div.body
div.bio { user.bio } div.bio { user.bio }
@ -57,36 +56,26 @@ style.
width calc(100% - 74px) width calc(100% - 74px)
> header > header
white-space nowrap
@media (min-width 500px) @media (min-width 500px)
margin-bottom 2px margin-bottom 2px
&:after > .name
content "" display inline
display block margin 0
clear both padding 0
color #777
font-size 1em
font-weight 700
text-align left
text-decoration none
> .left &:hover
float left text-decoration underline
> .name > .username
display inline text-align left
margin 0 margin 0 0 0 8px
padding 0 color #ccc
color #777
font-size 1em
font-weight 700
text-align left
text-decoration none
&:hover
text-decoration underline
> .username
text-align left
margin 0 0 0 8px
color #ccc
> .body > .body