From f9653647e484f721f86c6bbde0296c76e3adda65 Mon Sep 17 00:00:00 2001
From: AkiraFukushima
Date: Wed, 18 Mar 2020 00:21:57 +0900
Subject: [PATCH] refs #816 Add support for Misskey login
---
package-lock.json | 6 +-
package.json | 2 +-
spec/renderer/integration/store/Login.spec.ts | 3 +-
spec/renderer/unit/store/Login.spec.ts | 3 +-
src/main/account.ts | 2 +-
src/main/auth.ts | 68 +++++++++++++------
src/main/index.ts | 18 +++--
src/renderer/components/Authorize.vue | 13 +++-
src/renderer/components/Login/LoginForm.vue | 7 +-
src/renderer/router/index.ts | 2 +-
src/renderer/store/Authorize.ts | 13 +++-
src/renderer/store/Login.ts | 20 ++++--
12 files changed, 110 insertions(+), 47 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index a342e8bc..565213a3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16593,9 +16593,9 @@
"dev": true
},
"megalodon": {
- "version": "3.0.0-beta.4",
- "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-3.0.0-beta.4.tgz",
- "integrity": "sha512-u1uf4C0A/l/x/QDe29rR+fvmxak9j5cNGoN64Aypb7IsCzrX/3gASQjcK88H8AhOgxmogETQwEuGI1JAPIx/zw==",
+ "version": "3.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-3.0.0-rc.3.tgz",
+ "integrity": "sha512-EMXBBw/U0buIZzOBX0XhPvGJ8WTfo4I3Xxl5UQj3fGqUsdb3LJqCE232oo9zoAiktFP0JYbrEXgUkvG+w70R1w==",
"requires": {
"@types/oauth": "^0.9.0",
"@types/ws": "^7.2.0",
diff --git a/package.json b/package.json
index 5a41f325..9717c802 100644
--- a/package.json
+++ b/package.json
@@ -184,7 +184,7 @@
"hoek": "^6.1.3",
"i18next": "^19.0.3",
"lodash": "^4.17.15",
- "megalodon": "3.0.0-beta.4",
+ "megalodon": "3.0.0-rc.3",
"moment": "^2.24.0",
"mousetrap": "^1.6.3",
"nedb": "^1.8.0",
diff --git a/spec/renderer/integration/store/Login.spec.ts b/spec/renderer/integration/store/Login.spec.ts
index 6e871b83..95f20018 100644
--- a/spec/renderer/integration/store/Login.spec.ts
+++ b/spec/renderer/integration/store/Login.spec.ts
@@ -14,7 +14,8 @@ jest.mock('megalodon', () => ({
const state = (): LoginState => {
return {
selectedInstance: null,
- searching: false
+ searching: false,
+ sns: 'mastodon'
}
}
diff --git a/spec/renderer/unit/store/Login.spec.ts b/spec/renderer/unit/store/Login.spec.ts
index a35fb807..e6861b3b 100644
--- a/spec/renderer/unit/store/Login.spec.ts
+++ b/spec/renderer/unit/store/Login.spec.ts
@@ -6,7 +6,8 @@ describe('Login', () => {
beforeEach(() => {
state = {
selectedInstance: null,
- searching: false
+ searching: false,
+ sns: 'mastodon'
}
})
describe('changeInstance', () => {
diff --git a/src/main/account.ts b/src/main/account.ts
index bcf3cc4b..a16904a5 100644
--- a/src/main/account.ts
+++ b/src/main/account.ts
@@ -97,7 +97,7 @@ export default class Account {
listAccounts(): Promise> {
return new Promise((resolve, reject) => {
this.db
- .find({ accessToken: { $ne: '' } })
+ .find({ $and: [{ accessToken: { $ne: '' } }, { accessToken: { $ne: null } }] })
.sort({ order: 1 })
.exec((err, docs) => {
if (err) return reject(err)
diff --git a/src/main/auth.ts b/src/main/auth.ts
index 54132814..b6d24e07 100644
--- a/src/main/auth.ts
+++ b/src/main/auth.ts
@@ -1,45 +1,48 @@
-import generator, { ProxyConfig, detector } from 'megalodon'
+import generator, { ProxyConfig } from 'megalodon'
+import crypto from 'crypto'
import Account from './account'
import { LocalAccount } from '~/src/types/localAccount'
const appName = 'Whalebird'
const appURL = 'https://whalebird.org'
-const scopes = ['read', 'write', 'follow']
export default class Authentication {
private db: Account
- private baseURL: string
- private domain: string
- private clientId: string
- private clientSecret: string
+ private baseURL: string | null = null
+ private domain: string | null = null
+ private clientId: string | null = null
+ private clientSecret: string | null = null
+ private sessionToken: string | null = null
private protocol: 'http' | 'https'
constructor(accountDB: Account) {
this.db = accountDB
- this.baseURL = ''
- this.domain = ''
- this.clientId = ''
- this.clientSecret = ''
this.protocol = 'https'
}
setOtherInstance(domain: string) {
this.baseURL = `${this.protocol}://${domain}`
this.domain = domain
- this.clientId = ''
- this.clientSecret = ''
+ this.clientId = null
+ this.clientSecret = null
}
- async getAuthorizationUrl(domain = 'mastodon.social', proxy: ProxyConfig | false): Promise {
+ async getAuthorizationUrl(
+ sns: 'mastodon' | 'pleroma' | 'misskey',
+ domain: string = 'mastodon.social',
+ proxy: ProxyConfig | false
+ ): Promise {
this.setOtherInstance(domain)
- const sns = await detector(this.baseURL, proxy)
+ if (!this.baseURL || !this.domain) {
+ throw new Error('domain is required')
+ }
const client = generator(sns, this.baseURL, null, 'Whalebird', proxy)
const res = await client.registerApp(appName, {
- scopes: scopes,
website: appURL
})
this.clientId = res.clientId
this.clientSecret = res.clientSecret
+ this.sessionToken = res.session_token
const order = await this.db
.lastAccount()
@@ -53,11 +56,11 @@ export default class Authentication {
domain: this.domain,
clientId: this.clientId,
clientSecret: this.clientSecret,
- accessToken: '',
+ accessToken: null,
refreshToken: null,
- username: '',
+ username: null,
accountId: null,
- avatar: '',
+ avatar: null,
order: order
}
await this.db.insertAccount(local)
@@ -67,10 +70,24 @@ export default class Authentication {
return res.url
}
- async getAccessToken(code: string, proxy: ProxyConfig | false): Promise {
- const sns = await detector(this.baseURL, proxy)
+ async getAccessToken(sns: 'mastodon' | 'pleroma' | 'misskey', code: string | null, proxy: ProxyConfig | false): Promise {
+ if (!this.baseURL) {
+ throw new Error('domain is required')
+ }
+ if (!this.clientSecret) {
+ throw new Error('client secret is required')
+ }
const client = generator(sns, this.baseURL, null, 'Whalebird', proxy)
- const tokenData = await client.fetchAccessToken(this.clientId, this.clientSecret, code, 'urn:ietf:wg:oauth:2.0:oob')
+
+ // In Misskey session token is required instead of authentication code.
+ let authCode = code
+ if (!code) {
+ authCode = this.sessionToken
+ }
+ if (!authCode) {
+ throw new Error('auth code is required')
+ }
+ const tokenData = await client.fetchAccessToken(this.clientId, this.clientSecret, authCode, 'urn:ietf:wg:oauth:2.0:oob')
const search = {
baseURL: this.baseURL,
domain: this.domain,
@@ -78,7 +95,14 @@ export default class Authentication {
clientSecret: this.clientSecret
}
const rec = await this.db.searchAccount(search)
- const accessToken = tokenData.accessToken
+ let accessToken = tokenData.accessToken
+ // In misskey, access token is sha256(userToken + clientSecret)
+ if (sns === 'misskey') {
+ accessToken = crypto
+ .createHash('sha256')
+ .update(tokenData.accessToken + this.clientSecret, 'utf8')
+ .digest('hex')
+ }
const refreshToken = tokenData.refreshToken
const data = await this.db.fetchAccount(sns, rec, accessToken, proxy)
await this.db.updateAccount(rec._id!, {
diff --git a/src/main/index.ts b/src/main/index.ts
index 3e42ccf7..8fcacf29 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -364,10 +364,15 @@ app.on('activate', () => {
let auth = new Authentication(accountManager)
-ipcMain.on('get-auth-url', async (event: IpcMainEvent, domain: string) => {
+type AuthRequest = {
+ instance: string
+ sns: 'mastodon' | 'pleroma' | 'misskey'
+}
+
+ipcMain.on('get-auth-url', async (event: IpcMainEvent, request: AuthRequest) => {
const proxy = await proxyConfiguration.forMastodon()
auth
- .getAuthorizationUrl(domain, proxy)
+ .getAuthorizationUrl(request.sns, request.instance, proxy)
.then(url => {
log.debug(url)
event.sender.send('response-get-auth-url', url)
@@ -380,10 +385,15 @@ ipcMain.on('get-auth-url', async (event: IpcMainEvent, domain: string) => {
})
})
-ipcMain.on('get-access-token', async (event: IpcMainEvent, code: string) => {
+type TokenRequest = {
+ code: string | null
+ sns: 'mastodon' | 'pleroma' | 'misskey'
+}
+
+ipcMain.on('get-access-token', async (event: IpcMainEvent, request: TokenRequest) => {
const proxy = await proxyConfiguration.forMastodon()
auth
- .getAccessToken(code, proxy)
+ .getAccessToken(request.sns, request.code, proxy)
.then(token => {
accountDB.findOne(
{
diff --git a/src/renderer/components/Authorize.vue b/src/renderer/components/Authorize.vue
index 4c1a699e..62e02877 100644
--- a/src/renderer/components/Authorize.vue
+++ b/src/renderer/components/Authorize.vue
@@ -22,7 +22,7 @@
class="authorize-form"
v-on:submit.prevent="authorizeSubmit"
>
-
+
@@ -47,12 +47,16 @@ export default {
url: {
type: String,
default: ''
+ },
+ sns: {
+ type: String,
+ default: 'mastodon'
}
},
data() {
return {
authorizeForm: {
- code: ''
+ code: null
},
submitting: false
}
@@ -64,7 +68,10 @@ export default {
authorizeSubmit() {
this.submitting = true
this.$store
- .dispatch('Authorize/submit', this.authorizeForm.code)
+ .dispatch('Authorize/submit', {
+ code: this.authorizeForm.code,
+ sns: this.sns
+ })
.finally(() => {
this.submitting = false
})
diff --git a/src/renderer/components/Login/LoginForm.vue b/src/renderer/components/Login/LoginForm.vue
index 5f91e97e..3d01e92d 100644
--- a/src/renderer/components/Login/LoginForm.vue
+++ b/src/renderer/components/Login/LoginForm.vue
@@ -45,7 +45,8 @@ export default {
computed: {
...mapState({
selectedInstance: state => state.Login.selectedInstance,
- searching: state => state.Login.searching
+ searching: state => state.Login.searching,
+ sns: state => state.Login.sns
}),
allowLogin: function() {
return this.selectedInstance && this.form.domainName === this.selectedInstance
@@ -78,11 +79,11 @@ export default {
background: 'rgba(0, 0, 0, 0.7)'
})
this.$store
- .dispatch('Login/fetchLogin', this.selectedInstance)
+ .dispatch('Login/fetchLogin')
.then(url => {
loading.close()
this.$store.dispatch('Login/pageBack')
- this.$router.push({ path: '/authorize', query: { url: url } })
+ this.$router.push({ path: '/authorize', query: { url: url, sns: this.sns } })
})
.catch(() => {
loading.close()
diff --git a/src/renderer/router/index.ts b/src/renderer/router/index.ts
index ac82a23d..0a278c72 100644
--- a/src/renderer/router/index.ts
+++ b/src/renderer/router/index.ts
@@ -44,7 +44,7 @@ const router = new Router({
path: '/authorize',
name: 'authorize',
component: Authorize,
- props: route => ({ url: route.query.url })
+ props: route => ({ url: route.query.url, sns: route.query.sns })
},
{
path: '/preferences/',
diff --git a/src/renderer/store/Authorize.ts b/src/renderer/store/Authorize.ts
index 6f1dc0bf..7ed061ab 100644
--- a/src/renderer/store/Authorize.ts
+++ b/src/renderer/store/Authorize.ts
@@ -9,14 +9,23 @@ export type AuthorizeState = {}
const state = (): AuthorizeState => ({})
const actions: ActionTree = {
- submit: (_, code: string) => {
+ submit: (_, request: { code: string | null; sns: 'mastodon' | 'pleroma' | 'misskey' }) => {
return new Promise((resolve, reject) => {
- win.ipcRenderer.send('get-access-token', code.trim())
+ let req = {
+ sns: request.sns
+ }
+ if (request.code) {
+ req = Object.assign(req, {
+ code: request.code.trim()
+ })
+ }
+ win.ipcRenderer.send('get-access-token', req)
win.ipcRenderer.once('response-get-access-token', (_, id: string) => {
win.ipcRenderer.removeAllListeners('error-get-access-token')
resolve(id)
})
win.ipcRenderer.once('error-get-access-token', (_, err: Error) => {
+ console.error(err)
win.ipcRenderer.removeAllListeners('response-get-access-token')
reject(err)
})
diff --git a/src/renderer/store/Login.ts b/src/renderer/store/Login.ts
index cbdaead7..66abe8a3 100644
--- a/src/renderer/store/Login.ts
+++ b/src/renderer/store/Login.ts
@@ -8,16 +8,19 @@ const win = window as MyWindow
export type LoginState = {
selectedInstance: string | null
searching: boolean
+ sns: 'mastodon' | 'pleroma' | 'misskey'
}
const state = (): LoginState => ({
selectedInstance: null,
- searching: false
+ searching: false,
+ sns: 'mastodon'
})
export const MUTATION_TYPES = {
CHANGE_INSTANCE: 'changeInstance',
- CHANGE_SEARCHING: 'changeSearching'
+ CHANGE_SEARCHING: 'changeSearching',
+ CHANGE_SNS: 'changeSNS'
}
const mutations: MutationTree = {
@@ -26,13 +29,19 @@ const mutations: MutationTree = {
},
[MUTATION_TYPES.CHANGE_SEARCHING]: (state: LoginState, searching: boolean) => {
state.searching = searching
+ },
+ [MUTATION_TYPES.CHANGE_SNS]: (state: LoginState, sns: 'mastodon' | 'pleroma' | 'misskey') => {
+ state.sns = sns
}
}
const actions: ActionTree = {
- fetchLogin: (_, instance: string) => {
+ fetchLogin: ({ state }) => {
return new Promise((resolve, reject) => {
- win.ipcRenderer.send('get-auth-url', instance)
+ win.ipcRenderer.send('get-auth-url', {
+ instance: state.selectedInstance,
+ sns: state.sns
+ })
win.ipcRenderer.once('error-get-auth-url', (_, err: Error) => {
win.ipcRenderer.removeAllListeners('response-get-auth-url')
reject(err)
@@ -49,9 +58,10 @@ const actions: ActionTree = {
confirmInstance: async ({ commit, rootState }, domain: string): Promise => {
commit(MUTATION_TYPES.CHANGE_SEARCHING, true)
const cleanDomain = domain.trim()
- await detector(`https://${cleanDomain}`, rootState.App.proxyConfiguration)
+ const sns = await detector(`https://${cleanDomain}`, rootState.App.proxyConfiguration)
commit(MUTATION_TYPES.CHANGE_SEARCHING, false)
commit(MUTATION_TYPES.CHANGE_INSTANCE, cleanDomain)
+ commit(MUTATION_TYPES.CHANGE_SNS, sns)
return true
}
}