Merge pull request #925 from h3poteto/iss-859

refs #859 Update access token using refresh token when expire the token
This commit is contained in:
AkiraFukushima 2019-05-22 23:01:49 +09:00 committed by GitHub
commit 4ecec106e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 163 additions and 134 deletions

View File

@ -1,28 +1,29 @@
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import Mastodon, { Account as RemoteAccount } from 'megalodon' import Mastodon, { Account as RemoteAccount } from 'megalodon'
import Datastore from 'nedb' import Datastore from 'nedb'
import log from 'electron-log'
import LocalAccount from '~/src/types/localAccount' import LocalAccount from '~/src/types/localAccount'
export default class Account { export default class Account {
private db: Datastore private db: Datastore
constructor (db: Datastore) { constructor(db: Datastore) {
this.db = db this.db = db
} }
async initialize () { async initialize() {
await this.cleanup() await this.cleanup()
await this.reorder() await this.reorder()
await this.updateUnique() await this.updateUnique()
} }
updateUnique (): Promise<{}> { updateUnique(): Promise<{}> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// At first, remove old index. // At first, remove old index.
this.db.removeIndex('order', (err) => { this.db.removeIndex('order', err => {
if (err) reject(err) if (err) reject(err)
// Add unique index. // Add unique index.
this.db.ensureIndex({ fieldName: 'order', unique: true, sparse: true }, (err) => { this.db.ensureIndex({ fieldName: 'order', unique: true, sparse: true }, err => {
if (err) reject(err) if (err) reject(err)
resolve({}) resolve({})
}) })
@ -33,12 +34,14 @@ export default class Account {
/** /**
* Reorder accounts, because sometimes the order of accounts is duplicated. * Reorder accounts, because sometimes the order of accounts is duplicated.
*/ */
async reorder () { async reorder() {
const accounts = await this.listAllAccounts() const accounts = await this.listAllAccounts()
await Promise.all(accounts.map(async (account, index) => { await Promise.all(
const update = await this.updateAccount(account._id!, Object.assign(account, { order: index + 1 })) accounts.map(async (account, index) => {
return update const update = await this.updateAccount(account._id!, Object.assign(account, { order: index + 1 }))
})) return update
})
)
const ordered = await this.listAllAccounts() const ordered = await this.listAllAccounts()
return ordered return ordered
} }
@ -46,21 +49,23 @@ export default class Account {
/** /**
* Check order of all accounts, and fix if order is negative value or over the length. * Check order of all accounts, and fix if order is negative value or over the length.
*/ */
async cleanup () { async cleanup() {
const accounts = await this.listAccounts() const accounts = await this.listAccounts()
if (accounts.length < 1) { if (accounts.length < 1) {
return accounts.length return accounts.length
} }
if (accounts[0].order < 1 || accounts[accounts.length - 1].order > accounts.length) { if (accounts[0].order < 1 || accounts[accounts.length - 1].order > accounts.length) {
await Promise.all(accounts.map(async (element, index) => { await Promise.all(
const update = await this.updateAccount(element._id!, Object.assign(element, { order: index + 1 })) accounts.map(async (element, index) => {
return update const update = await this.updateAccount(element._id!, Object.assign(element, { order: index + 1 }))
})) return update
})
)
} }
return null return null
} }
insertAccount (localAccount: LocalAccount): Promise<LocalAccount> { insertAccount(localAccount: LocalAccount): Promise<LocalAccount> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.insert<LocalAccount>(localAccount, (err, doc) => { this.db.insert<LocalAccount>(localAccount, (err, doc) => {
if (err) return reject(err) if (err) return reject(err)
@ -73,36 +78,42 @@ export default class Account {
/** /**
* List up all accounts either authenticated or not authenticated. * List up all accounts either authenticated or not authenticated.
*/ */
listAllAccounts (order = 1): Promise<Array<LocalAccount>> { listAllAccounts(order = 1): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.find<LocalAccount>({}).sort({ order: order }).exec((err, docs) => { this.db
if (err) return reject(err) .find<LocalAccount>({})
if (isEmpty(docs)) return reject(new EmptyRecordError('empty')) .sort({ order: order })
resolve(docs) .exec((err, docs) => {
}) if (err) return reject(err)
if (isEmpty(docs)) return reject(new EmptyRecordError('empty'))
resolve(docs)
})
}) })
} }
/** /**
* List up authenticated accounts. * List up authenticated accounts.
*/ */
listAccounts (): Promise<Array<LocalAccount>> { listAccounts(): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.find<LocalAccount>({ accessToken: { $ne: '' } }).sort({ order: 1 }).exec((err, docs) => { this.db
if (err) return reject(err) .find<LocalAccount>({ accessToken: { $ne: '' } })
if (isEmpty(docs)) return reject(new EmptyRecordError('empty')) .sort({ order: 1 })
resolve(docs) .exec((err, docs) => {
}) if (err) return reject(err)
if (isEmpty(docs)) return reject(new EmptyRecordError('empty'))
resolve(docs)
})
}) })
} }
// Get the last account. // Get the last account.
async lastAccount (): Promise<LocalAccount> { async lastAccount(): Promise<LocalAccount> {
const accounts = await this.listAllAccounts(-1) const accounts = await this.listAllAccounts(-1)
return accounts[0] return accounts[0]
} }
getAccount (id: string): Promise<LocalAccount> { getAccount(id: string): Promise<LocalAccount> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.findOne<LocalAccount>( this.db.findOne<LocalAccount>(
{ {
@ -117,28 +128,29 @@ export default class Account {
}) })
} }
searchAccount (obj: any): Promise<LocalAccount> { searchAccount(obj: any): Promise<LocalAccount> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.findOne<LocalAccount>( this.db.findOne<LocalAccount>(obj, (err, doc) => {
obj,
(err, doc) => {
if (err) return reject(err)
if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc)
})
})
}
searchAccounts (obj: any, order = 1): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => {
this.db.find<LocalAccount>(obj).sort({ order: order }).exec((err, docs) => {
if (err) return reject(err) if (err) return reject(err)
resolve(docs) if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc)
}) })
}) })
} }
updateAccount (id: string, obj: any): Promise<LocalAccount> { searchAccounts(obj: any, order = 1): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => {
this.db
.find<LocalAccount>(obj)
.sort({ order: order })
.exec((err, docs) => {
if (err) return reject(err)
resolve(docs)
})
})
}
updateAccount(id: string, obj: any): Promise<LocalAccount> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.update( this.db.update(
{ {
@ -156,13 +168,14 @@ export default class Account {
if (err) return reject(err) if (err) return reject(err)
if (isEmpty(doc)) return reject(new EmptyRecordError('empty')) if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc) resolve(doc)
}) }
)
} }
) )
}) })
} }
removeAccount (id: string): Promise<number> { removeAccount(id: string): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.remove( this.db.remove(
{ {
@ -177,26 +190,21 @@ export default class Account {
}) })
} }
removeAll (): Promise<number> { removeAll(): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.remove( this.db.remove({}, { multi: true }, (err, numRemoved) => {
{}, if (err) return reject(err)
{ multi: true }, resolve(numRemoved)
(err, numRemoved) => { })
if (err) return reject(err)
resolve(numRemoved)
}
)
}) })
} }
async forwardAccount (ac: LocalAccount): Promise<LocalAccount | {}> { async forwardAccount(ac: LocalAccount): Promise<LocalAccount | {}> {
// Find account which is the previous of the target account. // Find account which is the previous of the target account.
const accounts = await this.searchAccounts({ order: { $lt: ac.order } }, -1) const accounts = await this.searchAccounts({ order: { $lt: ac.order } }, -1).catch(err => {
.catch((err) => { console.log(err)
console.log(err) return []
return [] })
})
if (accounts.length < 1) { if (accounts.length < 1) {
return Promise.resolve({}) return Promise.resolve({})
} }
@ -206,36 +214,35 @@ export default class Account {
// At first, we need to update the previous account with dummy order. // At first, we need to update the previous account with dummy order.
// Because this column is uniqued, so can not update with same order. // Because this column is uniqued, so can not update with same order.
await this.updateAccount(previousAccount._id!, Object.assign( await this.updateAccount(
previousAccount, previousAccount._id!,
{ Object.assign(previousAccount, {
order: -1 order: -1
} })
)) )
// Change order of the target account. // Change order of the target account.
const updated = await this.updateAccount(ac._id!, Object.assign( const updated = await this.updateAccount(
ac, ac._id!,
{ Object.assign(ac, {
order: previousOrder order: previousOrder
} })
)) )
// Update the previous account with right order. // Update the previous account with right order.
await this.updateAccount(previousAccount._id!, Object.assign( await this.updateAccount(
previousAccount, previousAccount._id!,
{ Object.assign(previousAccount, {
order: targetOrder order: targetOrder
} })
)) )
return updated return updated
} }
async backwardAccount (ac: LocalAccount): Promise<LocalAccount | {}> { async backwardAccount(ac: LocalAccount): Promise<LocalAccount | {}> {
// Find account which is the next of the target account. // Find account which is the next of the target account.
const accounts = await this.searchAccounts({ order: { $gt: ac.order } }, 1) const accounts = await this.searchAccounts({ order: { $gt: ac.order } }, 1).catch(err => {
.catch((err) => { console.log(err)
console.log(err) return []
return [] })
})
if (accounts.length < 1) { if (accounts.length < 1) {
return Promise.resolve({}) return Promise.resolve({})
} }
@ -245,63 +252,82 @@ export default class Account {
// At first, we need to update the next account with dummy order. // At first, we need to update the next account with dummy order.
// Because this colum is uniqued, so can not update with same order. // Because this colum is uniqued, so can not update with same order.
await this.updateAccount(nextAccount._id!, Object.assign( await this.updateAccount(
nextAccount, nextAccount._id!,
{ Object.assign(nextAccount, {
order: -1 order: -1
} })
)) )
// Change order of the target account/ // Change order of the target account/
const updated = await this.updateAccount(ac._id!, Object.assign( const updated = await this.updateAccount(
ac, ac._id!,
{ Object.assign(ac, {
order: nextOrder order: nextOrder
} })
)) )
// Update the next account with right order. // Update the next account with right order.
await this.updateAccount(nextAccount._id!, Object.assign( await this.updateAccount(
nextAccount, nextAccount._id!,
{ Object.assign(nextAccount, {
order: targetOrder order: targetOrder
} })
)) )
return updated return updated
} }
async refreshAccounts (): Promise<Array<LocalAccount>> { async refreshAccounts(): Promise<Array<LocalAccount>> {
const accounts = await this.listAccounts() const accounts = await this.listAccounts()
if (accounts.length < 1) { if (accounts.length < 1) {
return accounts return accounts
} }
const results = await Promise.all(accounts.map(async (account) => { const results = await Promise.all(
const refresh = await this.refresh(account) accounts.map(async account => {
return refresh const refresh = await this.refresh(account)
})) return refresh
})
)
return results return results
} }
async refresh (account: LocalAccount): Promise<LocalAccount> { /**
const client = new Mastodon( * refresh: Refresh an account which is already saved at local
account.accessToken!, * @param {LocalAccount} account is an local account
account.baseURL + '/api/v1' * @return {LocalAccount} updated account
) */
return client.get<RemoteAccount>('/accounts/verify_credentials') async refresh(account: LocalAccount): Promise<LocalAccount> {
.then(res => { let client = new Mastodon(account.accessToken!, account.baseURL + '/api/v1')
const json = { let json = {}
username: res.data.username, try {
accountId: res.data.id, const res = await client.get<RemoteAccount>('/accounts/verify_credentials')
avatar: res.data.avatar json = {
} username: res.data.username,
return this.updateAccount(account._id!, json) accountId: res.data.id,
}) avatar: res.data.avatar
}
} catch (err) {
log.error(err)
log.info('Get new access token using refresh token...')
// If failed to fetch account, get new access token usign refresh token.
if (!account.refreshToken) {
throw new RefreshTokenDoesNotExist()
}
const token = await Mastodon.refreshToken(account.clientId, account.clientSecret, account.refreshToken, account.baseURL)
client = new Mastodon(token.access_token, account.baseURL + '/api/v1')
const res = await client.get<RemoteAccount>('/accounts/verify_credentials')
json = {
username: res.data.username,
accountId: res.data.id,
avatar: res.data.avatar,
accessToken: token.accessToken,
refreshToken: token.refreshToken
}
}
return this.updateAccount(account._id!, json)
} }
// Confirm the access token, and check duplicate // Confirm the access token, and check duplicate
async fetchAccount (account: LocalAccount, accessToken: string): Promise<RemoteAccount> { async fetchAccount(account: LocalAccount, accessToken: string): Promise<RemoteAccount> {
const client = new Mastodon( const client = new Mastodon(accessToken, account.baseURL + '/api/v1')
accessToken,
account.baseURL + '/api/v1'
)
const res = await client.get<RemoteAccount>('/accounts/verify_credentials') const res = await client.get<RemoteAccount>('/accounts/verify_credentials')
const query = { const query = {
baseURL: account.baseURL, baseURL: account.baseURL,
@ -318,3 +344,5 @@ export default class Account {
class EmptyRecordError extends Error {} class EmptyRecordError extends Error {}
class DuplicateRecordError extends Error {} class DuplicateRecordError extends Error {}
class RefreshTokenDoesNotExist extends Error {}

View File

@ -1,4 +1,4 @@
import Mastodon from 'megalodon' import Mastodon, { OAuth } from 'megalodon'
import Account from './account' import Account from './account'
import LocalAccount from '~/src/types/localAccount' import LocalAccount from '~/src/types/localAccount'
@ -14,7 +14,7 @@ export default class Authentication {
private clientSecret: string private clientSecret: string
private protocol: 'http' | 'https' private protocol: 'http' | 'https'
constructor (accountDB: Account) { constructor(accountDB: Account) {
this.db = accountDB this.db = accountDB
this.baseURL = '' this.baseURL = ''
this.domain = '' this.domain = ''
@ -23,17 +23,18 @@ export default class Authentication {
this.protocol = 'https' this.protocol = 'https'
} }
setOtherInstance (domain: string) { setOtherInstance(domain: string) {
this.baseURL = `${this.protocol}://${domain}` this.baseURL = `${this.protocol}://${domain}`
this.domain = domain this.domain = domain
this.clientId = '' this.clientId = ''
this.clientSecret = '' this.clientSecret = ''
} }
async getAuthorizationUrl (domain = 'mastodon.social'): Promise<string> { async getAuthorizationUrl(domain = 'mastodon.social'): Promise<string> {
this.setOtherInstance(domain) this.setOtherInstance(domain)
const res = await Mastodon.registerApp( const res = await Mastodon.registerApp(
appName, { appName,
{
scopes: scope, scopes: scope,
website: appURL website: appURL
}, },
@ -42,9 +43,10 @@ export default class Authentication {
this.clientId = res.clientId this.clientId = res.clientId
this.clientSecret = res.clientSecret this.clientSecret = res.clientSecret
const order = await this.db.lastAccount() const order = await this.db
.lastAccount()
.then(account => account.order + 1) .then(account => account.order + 1)
.catch((err) => { .catch(err => {
console.log(err) console.log(err)
return 1 return 1
}) })
@ -54,7 +56,7 @@ export default class Authentication {
clientId: this.clientId, clientId: this.clientId,
clientSecret: this.clientSecret, clientSecret: this.clientSecret,
accessToken: '', accessToken: '',
refreshToken: '', refreshToken: null,
username: '', username: '',
accountId: null, accountId: null,
avatar: '', avatar: '',
@ -67,8 +69,8 @@ export default class Authentication {
return res.url return res.url
} }
async getAccessToken (code: string): Promise<string> { async getAccessToken(code: string): Promise<string> {
const tokenData = await Mastodon.fetchAccessToken(this.clientId, this.clientSecret, code, this.baseURL) const tokenData: OAuth.TokenData = await Mastodon.fetchAccessToken(this.clientId, this.clientSecret, code, this.baseURL)
const search = { const search = {
baseURL: this.baseURL, baseURL: this.baseURL,
domain: this.domain, domain: this.domain,
@ -88,7 +90,6 @@ export default class Authentication {
}) })
return accessToken return accessToken
} }
// TODO: Refresh access token when expired
} }
class AuthenticationURLError extends Error {} class AuthenticationURLError extends Error {}