[refactor] Remove authorize store

This commit is contained in:
AkiraFukushima 2023-02-12 17:50:38 +09:00
parent b46c9c2e91
commit df5ecdf9fd
No known key found for this signature in database
GPG Key ID: B6E51BAC4DE1A957
11 changed files with 139 additions and 210 deletions

View File

@ -11,7 +11,7 @@ export const insertAccount = (
clientSecret: string, clientSecret: string,
accessToken: string, accessToken: string,
refreshToken: string | null, refreshToken: string | null,
server: LocalServer serverId: number
): Promise<LocalAccount> => { ): Promise<LocalAccount> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.serialize(() => { db.serialize(() => {
@ -34,13 +34,12 @@ export const insertAccount = (
} }
const id = this.lastID const id = this.lastID
db.run('UPDATE servers SET account_id = ? WHERE id = ?', [id, server.id], err => { db.run('UPDATE servers SET account_id = ? WHERE id = ?', [id, serverId], err => {
if (err) { if (err) {
reject(err) reject(err)
} }
db.run('COMMIT') db.run('COMMIT')
resolve({ resolve({
id, id,
username, username,
@ -164,6 +163,9 @@ FROM accounts INNER JOIN servers ON servers.account_id = accounts.id WHERE accou
export const removeAccount = (db: sqlite3.Database, id: number): Promise<null> => { export const removeAccount = (db: sqlite3.Database, id: number): Promise<null> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('PRAGMA foreign_keys = ON')
db.run('DELETE FROM accounts WHERE id = ?', id, err => { db.run('DELETE FROM accounts WHERE id = ?', id, err => {
if (err) { if (err) {
reject(err) reject(err)
@ -171,10 +173,14 @@ export const removeAccount = (db: sqlite3.Database, id: number): Promise<null> =
resolve(null) resolve(null)
}) })
}) })
})
} }
export const removeAllAccounts = (db: sqlite3.Database): Promise<null> => { export const removeAllAccounts = (db: sqlite3.Database): Promise<null> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('PRAGMA foreign_keys = ON')
db.run('DELETE FROM accounts', err => { db.run('DELETE FROM accounts', err => {
if (err) { if (err) {
reject(err) reject(err)
@ -182,6 +188,7 @@ export const removeAllAccounts = (db: sqlite3.Database): Promise<null> => {
resolve(null) resolve(null)
}) })
}) })
})
} }
export const forwardAccount = (db: sqlite3.Database, id: number): Promise<null> => { export const forwardAccount = (db: sqlite3.Database, id: number): Promise<null> => {

View File

@ -53,6 +53,9 @@ export const insertTag = (db: sqlite3.Database, accountId: number, tag: string):
export const removeTag = (db: sqlite3.Database, tag: LocalTag): Promise<null> => { export const removeTag = (db: sqlite3.Database, tag: LocalTag): Promise<null> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.serialize(() => {
db.run('PRAGMA foreign_keys = ON')
db.run('DELETE FROM hashtags WHERE id = ?', tag.id, err => { db.run('DELETE FROM hashtags WHERE id = ?', tag.id, err => {
if (err) { if (err) {
reject(err) reject(err)
@ -60,4 +63,5 @@ export const removeTag = (db: sqlite3.Database, tag: LocalTag): Promise<null> =>
resolve(null) resolve(null)
}) })
}) })
})
} }

View File

@ -463,26 +463,28 @@ ipcMain.handle('add-app', async (_: IpcMainInvokeEvent, url: string) => {
}) })
type AuthorizeRequest = { type AuthorizeRequest = {
server: LocalServer serverID: number
appData: OAuth.AppData baseURL: string
clientID: string
clientSecret: string
code: string code: string
} }
ipcMain.handle('authorize', async (_: IpcMainInvokeEvent, req: AuthorizeRequest) => { ipcMain.handle('authorize', async (_: IpcMainInvokeEvent, req: AuthorizeRequest) => {
const proxy = await proxyConfiguration.forMastodon() const proxy = await proxyConfiguration.forMastodon()
const sns = await detector(req.server.baseURL, proxy) const sns = await detector(req.baseURL, proxy)
const client = generator(sns, req.server.baseURL, null, 'Whalebird', proxy) const client = generator(sns, req.baseURL, null, 'Whalebird', proxy)
const tokenData = await client.fetchAccessToken(req.appData.client_id, req.appData.client_secret, req.code, 'urn:ietf:wg:oauth:2.0:oob') const tokenData = await client.fetchAccessToken(req.clientID, req.clientSecret, req.code, 'urn:ietf:wg:oauth:2.0:oob')
let accessToken = tokenData.access_token let accessToken = tokenData.access_token
if (sns === 'misskey') { if (sns === 'misskey') {
// In misskey, access token is sha256(userToken + clientSecret) // In misskey, access token is sha256(userToken + clientSecret)
accessToken = crypto accessToken = crypto
.createHash('sha256') .createHash('sha256')
.update(tokenData.access_token + req.appData.client_secret, 'utf8') .update(tokenData.access_token + req.clientSecret, 'utf8')
.digest('hex') .digest('hex')
} }
const authorizedClient = generator(sns, req.server.baseURL, accessToken, 'Whalebird', proxy) const authorizedClient = generator(sns, req.baseURL, accessToken, 'Whalebird', proxy)
const credentials = await authorizedClient.verifyAccountCredentials() const credentials = await authorizedClient.verifyAccountCredentials()
const account = await insertAccount( const account = await insertAccount(
@ -490,11 +492,11 @@ ipcMain.handle('authorize', async (_: IpcMainInvokeEvent, req: AuthorizeRequest)
credentials.data.username, credentials.data.username,
credentials.data.id, credentials.data.id,
credentials.data.avatar, credentials.data.avatar,
req.appData.client_id, req.clientID,
req.appData.client_secret, req.clientSecret,
accessToken, accessToken,
tokenData.refresh_token, tokenData.refresh_token,
req.server req.serverID
) )
return account return account
}) })

View File

@ -24,7 +24,7 @@
<FailoverImg :src="`${server.baseURL}/favicon.ico`" :failoverSrc="`${server.baseURL}/favicon.png`" class="instance-icon" /> <FailoverImg :src="`${server.baseURL}/favicon.ico`" :failoverSrc="`${server.baseURL}/favicon.png`" class="instance-icon" />
<span>{{ server.domain }}</span> <span>{{ server.domain }}</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="/login" :title="$t('global_header.add_new_account')" role="menuitem" class="add-new-account"> <el-menu-item index="/login/form" :title="$t('global_header.add_new_account')" role="menuitem" class="add-new-account">
<font-awesome-icon icon="plus" /> <font-awesome-icon icon="plus" />
<span>New</span> <span>New</span>
</el-menu-item> </el-menu-item>
@ -71,7 +71,7 @@ export default defineComponent({
} }
}) })
.catch(_ => { .catch(_ => {
return router.push({ path: '/login' }) return router.push({ path: '/login/form' })
}) })
} }

View File

@ -10,38 +10,27 @@
</el-row> </el-row>
</el-header> </el-header>
<el-container> <el-container>
<login-form v-if="appData === null" /> <router-view />
<authorize v-else />
</el-container> </el-container>
</el-container> </el-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed } from 'vue' import { defineComponent } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useStore } from '@/store'
import { useMagicKeys, whenever } from '@vueuse/core' import { useMagicKeys, whenever } from '@vueuse/core'
import LoginForm from './Login/LoginForm.vue'
import Authorize from './Login/Authorize.vue'
import { ACTION_TYPES } from '@/store/Login'
export default defineComponent({ export default defineComponent({
name: 'login', name: 'login',
components: { LoginForm, Authorize },
setup() { setup() {
const space = 'Login'
const store = useStore()
const router = useRouter() const router = useRouter()
const { escape } = useMagicKeys() const { escape } = useMagicKeys()
const appData = computed(() => store.state.Login.appData)
whenever(escape, () => { whenever(escape, () => {
close() close()
}) })
const close = () => { const close = () => {
store.dispatch(`${space}/${ACTION_TYPES.PAGE_BACK}`)
return router.push({ return router.push({
path: '/', path: '/',
query: { redirect: 'home' } query: { redirect: 'home' }
@ -49,8 +38,7 @@ export default defineComponent({
} }
return { return {
close, close
appData
} }
} }
}) })

View File

@ -13,7 +13,7 @@
class="authorize-form" class="authorize-form"
@submit.prevent="authorizeSubmit" @submit.prevent="authorizeSubmit"
> >
<p v-if="sns === 'misskey'">{{ $t('authorize.misskey_label') }}</p> <p v-if="$route.query.sns === 'misskey'">{{ $t('authorize.misskey_label') }}</p>
<el-form-item :label="$t('authorize.code_label')" v-else> <el-form-item :label="$t('authorize.code_label')" v-else>
<el-input v-model="authorizeForm.code"></el-input> <el-input v-model="authorizeForm.code"></el-input>
</el-form-item> </el-form-item>
@ -37,27 +37,25 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, reactive, computed } from 'vue' import { defineComponent, ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useI18next } from 'vue3-i18next' import { useI18next } from 'vue3-i18next'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useMagicKeys, whenever } from '@vueuse/core' import { useMagicKeys, whenever } from '@vueuse/core'
import { useStore } from '@/store' import { MyWindow } from '~/src/types/global'
import { ACTION_TYPES } from '@/store/Login' import { LocalAccount } from '~/src/types/localAccount'
export default defineComponent({ export default defineComponent({
name: 'Authorize', name: 'Authorize',
setup() { setup() {
const space = 'Login' const win = (window as any) as MyWindow
const store = useStore()
const router = useRouter() const router = useRouter()
const route = useRoute()
const i18n = useI18next() const i18n = useI18next()
const { escape } = useMagicKeys() const { escape } = useMagicKeys()
const sns = computed(() => store.state.Login.sns)
const authorizeForm = reactive({ const authorizeForm = reactive({
code: null code: ''
}) })
const submitting = ref<boolean>(false) const submitting = ref<boolean>(false)
@ -65,23 +63,30 @@ export default defineComponent({
close() close()
}) })
const authorizeSubmit = () => { const authorizeSubmit = async () => {
submitting.value = true submitting.value = true
store let code = authorizeForm.code
.dispatch(`${space}/${ACTION_TYPES.AUTHORIZE}`, authorizeForm.code) if (route.query.sns === 'misskey' && route.query.session_token) {
.finally(() => { code = route.query.session_token.toString()
submitting.value = false }
try {
const localAccount: LocalAccount = await win.ipcRenderer.invoke('authorize', {
serverID: route.query.server_id,
baseURL: route.query.base_url,
clientID: route.query.client_id,
clientSecret: route.query.client_secret,
code: code
}) })
.then(id => { router.push({ path: `/${localAccount.id}/home` })
router.push({ path: `/${id}/home` }) } catch (err) {
})
.catch(err => {
console.error(err) console.error(err)
ElMessage({ ElMessage({
message: i18n.t('message.authorize_error'), message: i18n.t('message.authorize_error'),
type: 'error' type: 'error'
}) })
}) } finally {
submitting.value = false
}
} }
const close = () => { const close = () => {
@ -92,8 +97,7 @@ export default defineComponent({
authorizeForm, authorizeForm,
submitting, submitting,
authorizeSubmit, authorizeSubmit,
close, close
sns
} }
} }
}) })

View File

@ -40,25 +40,28 @@
import { defineComponent, computed, reactive, ref } from 'vue' import { defineComponent, computed, reactive, ref } from 'vue'
import { useI18next } from 'vue3-i18next' import { useI18next } from 'vue3-i18next'
import { ElLoading, ElMessage, FormInstance, FormRules } from 'element-plus' import { ElLoading, ElMessage, FormInstance, FormRules } from 'element-plus'
import { useStore } from '@/store'
import { domainFormat } from '@/utils/validator' import { domainFormat } from '@/utils/validator'
import { ACTION_TYPES } from '@/store/Login' import { detector, OAuth } from 'megalodon'
import { MyWindow } from '~/src/types/global'
import { useRouter } from 'vue-router'
import { LocalServer } from '~/src/types/localServer'
export default defineComponent({ export default defineComponent({
name: 'login-form', name: 'login-form',
setup() { setup() {
const space = 'Login'
const store = useStore()
const i18n = useI18next() const i18n = useI18next()
const router = useRouter()
const win = (window as any) as MyWindow
const form = reactive({ const form = reactive({
domainName: '' domainName: ''
}) })
const loginFormRef = ref<FormInstance>() const loginFormRef = ref<FormInstance>()
const domain = ref<string>('')
const searching = ref<boolean>(false)
const allowLogin = computed(() => domain.value && form.domainName == domain.value)
const sns = ref<'mastodon' | 'pleroma' | 'misskey'>('mastodon')
const selectedInstance = computed(() => store.state.Login.domain)
const searching = computed(() => store.state.Login.searching)
const allowLogin = computed(() => selectedInstance.value && form.domainName === selectedInstance.value)
const rules = reactive<FormRules>({ const rules = reactive<FormRules>({
domainName: [ domainName: [
{ {
@ -81,8 +84,20 @@ export default defineComponent({
background: 'rgba(0, 0, 0, 0.7)' background: 'rgba(0, 0, 0, 0.7)'
}) })
try { try {
await store.dispatch(`${space}/${ACTION_TYPES.ADD_SERVER}`) const server: LocalServer = await win.ipcRenderer.invoke('add-server', domain.value)
await store.dispatch(`${space}/${ACTION_TYPES.ADD_APP}`) const appData: OAuth.AppData = await win.ipcRenderer.invoke('add-app', `https://${domain.value}`)
router.push({
path: '/login/authorize',
query: {
server_id: server.id,
base_url: server.baseURL,
client_id: appData.client_id,
client_secret: appData.client_secret,
session_token: appData.session_token,
sns: sns.value,
url: appData.url
}
})
} catch (err) { } catch (err) {
ElMessage({ ElMessage({
message: i18n.t('message.authorize_url_error'), message: i18n.t('message.authorize_url_error'),
@ -96,27 +111,31 @@ export default defineComponent({
const confirm = async (formEl: FormInstance | undefined) => { const confirm = async (formEl: FormInstance | undefined) => {
if (!formEl) return if (!formEl) return
await formEl.validate(valid => { await formEl.validate(async valid => {
if (valid) { if (valid) {
return store searching.value = true
.dispatch(`${space}/${ACTION_TYPES.CONFIRM_INSTANCE}`, form.domainName) try {
.then(() => { const cleanDomain = form.domainName.trim()
sns.value = await detector(`https://${cleanDomain}`)
domain.value = cleanDomain
ElMessage({ ElMessage({
message: i18n.t('message.domain_confirmed', { message: i18n.t('message.domain_confirmed', {
domain: form.domainName domain: cleanDomain
}), }),
type: 'success' type: 'success'
}) })
}) } catch (err) {
.catch(err => { console.error(err)
ElMessage({ ElMessage({
message: i18n.t('message.domain_doesnt_exist', { message: i18n.t('message.domain_doesnt_exist', {
domain: form.domainName domain: form.domainName
}), }),
type: 'error' type: 'error'
}) })
console.error(err) } finally {
}) searching.value = false
}
return true
} else { } else {
ElMessage({ ElMessage({
message: i18n.t('validation.login.domain_format'), message: i18n.t('validation.login.domain_format'),

View File

@ -153,7 +153,7 @@ export default defineComponent({
const removeAllAssociations = () => { const removeAllAssociations = () => {
store.dispatch(`${space}/${ACTION_TYPES.REMOVE_ALL_ACCOUNTS}`).then(() => { store.dispatch(`${space}/${ACTION_TYPES.REMOVE_ALL_ACCOUNTS}`).then(() => {
router.push('/login') router.push('/login/form')
}) })
} }

View File

@ -1,6 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/components/Login.vue' import Login from '@/components/Login.vue'
import LoginForm from '@/components/Login/LoginForm.vue'
import Authorize from '@/components/Login/Authorize.vue'
import Preferences from '@/components/Preferences.vue' import Preferences from '@/components/Preferences.vue'
import PreferencesGeneral from '@/components/Preferences/General.vue' import PreferencesGeneral from '@/components/Preferences/General.vue'
import PreferencesAppearance from '@/components/Preferences/Appearance.vue' import PreferencesAppearance from '@/components/Preferences/Appearance.vue'
@ -34,9 +36,21 @@ import TimelineSpaceContentsBookmarks from '@/components/TimelineSpace/Contents/
const routes = [ const routes = [
{ {
path: '/login', path: '/login/',
name: 'login', name: 'login',
component: Login component: Login,
children: [
{
path: 'form',
name: 'login-form',
component: LoginForm
},
{
path: 'authorize',
name: 'authorize',
component: Authorize
}
]
}, },
{ {
path: '/preferences/', path: '/preferences/',

View File

@ -1,106 +0,0 @@
import { Module, MutationTree, ActionTree } from 'vuex'
import { detector } from 'megalodon'
import { RootState } from '@/store'
import { MyWindow } from '~/src/types/global'
import { LocalServer } from '~src/types/localServer'
import { OAuth } from 'megalodon'
import { LocalAccount } from '~src/types/localAccount'
import { toRaw } from 'vue'
const win = (window as any) as MyWindow
export type LoginState = {
domain: string | null
searching: boolean
server: LocalServer | null
appData: OAuth.AppData | null
sns: 'mastodon' | 'pleroma' | 'misskey'
}
const state = (): LoginState => ({
domain: null,
searching: false,
server: null,
appData: null,
sns: 'mastodon'
})
export const MUTATION_TYPES = {
CHANGE_DOMAIN: 'changeDomain',
CHANGE_SEARCHING: 'changeSearching',
CHANGE_SERVER: 'changeServer',
CHANGE_APP_DATA: 'changeAppData',
CHANGE_SNS: 'changeSNS'
}
const mutations: MutationTree<LoginState> = {
[MUTATION_TYPES.CHANGE_DOMAIN]: (state: LoginState, instance: string | null) => {
state.domain = instance
},
[MUTATION_TYPES.CHANGE_SEARCHING]: (state: LoginState, searching: boolean) => {
state.searching = searching
},
[MUTATION_TYPES.CHANGE_SERVER]: (state: LoginState, server: LocalServer | null) => {
state.server = server
},
[MUTATION_TYPES.CHANGE_APP_DATA]: (state: LoginState, appData: OAuth.AppData | null) => {
state.appData = appData
},
[MUTATION_TYPES.CHANGE_SNS]: (state: LoginState, sns: 'mastodon' | 'pleroma' | 'misskey') => {
state.sns = sns
}
}
export const ACTION_TYPES = {
ADD_SERVER: 'addServer',
ADD_APP: 'addApp',
AUTHORIZE: 'authorize',
PAGE_BACK: 'pageBack',
CONFIRM_INSTANCE: 'confirmInstance'
}
const actions: ActionTree<LoginState, RootState> = {
[ACTION_TYPES.ADD_SERVER]: async ({ state, commit }): Promise<LocalServer> => {
const server = await win.ipcRenderer.invoke('add-server', state.domain)
commit(MUTATION_TYPES.CHANGE_SERVER, server)
return server
},
[ACTION_TYPES.ADD_APP]: async ({ state, commit }) => {
const appData = await win.ipcRenderer.invoke('add-app', `https://${state.domain}`)
commit(MUTATION_TYPES.CHANGE_APP_DATA, appData)
},
[ACTION_TYPES.AUTHORIZE]: async ({ state }, code: string): Promise<number> => {
const localAccount: LocalAccount = await win.ipcRenderer.invoke('authorize', {
server: toRaw(state.server),
appData: toRaw(state.appData),
code
})
return localAccount.id
},
[ACTION_TYPES.PAGE_BACK]: ({ commit }) => {
commit(MUTATION_TYPES.CHANGE_DOMAIN, null)
commit(MUTATION_TYPES.CHANGE_SERVER, null)
commit(MUTATION_TYPES.CHANGE_APP_DATA, null)
},
[ACTION_TYPES.CONFIRM_INSTANCE]: async ({ commit }, domain: string): Promise<boolean> => {
commit(MUTATION_TYPES.CHANGE_SEARCHING, true)
const cleanDomain = domain.trim()
try {
const sns = await detector(`https://${cleanDomain}`)
commit(MUTATION_TYPES.CHANGE_DOMAIN, cleanDomain)
commit(MUTATION_TYPES.CHANGE_SNS, sns)
} finally {
commit(MUTATION_TYPES.CHANGE_SEARCHING, false)
}
return true
}
}
const Login: Module<LoginState, RootState> = {
namespaced: true,
state: state,
mutations: mutations,
actions: actions
}
export default Login

View File

@ -4,7 +4,6 @@ import { InjectionKey } from 'vue'
import App, { AppState } from './App' import App, { AppState } from './App'
import GlobalHeader, { GlobalHeaderState } from './GlobalHeader' import GlobalHeader, { GlobalHeaderState } from './GlobalHeader'
import Login, { LoginState } from './Login'
import TimelineSpace, { TimelineSpaceModuleState } from './TimelineSpace' import TimelineSpace, { TimelineSpaceModuleState } from './TimelineSpace'
import Preferences, { PreferencesModuleState } from './Preferences' import Preferences, { PreferencesModuleState } from './Preferences'
import Settings, { SettingsModuleState } from './Settings' import Settings, { SettingsModuleState } from './Settings'
@ -15,7 +14,6 @@ const win = (window as any) as MyWindow
export interface RootState { export interface RootState {
App: AppState App: AppState
GlobalHeader: GlobalHeaderState GlobalHeader: GlobalHeaderState
Login: LoginState
TimelineSpace: TimelineSpaceModuleState TimelineSpace: TimelineSpaceModuleState
Preferences: PreferencesModuleState Preferences: PreferencesModuleState
Settings: SettingsModuleState Settings: SettingsModuleState
@ -34,7 +32,6 @@ export default createStore({
modules: { modules: {
App, App,
GlobalHeader, GlobalHeader,
Login,
TimelineSpace, TimelineSpace,
Preferences, Preferences,
Settings Settings