[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,
accessToken: string,
refreshToken: string | null,
server: LocalServer
serverId: number
): Promise<LocalAccount> => {
return new Promise((resolve, reject) => {
db.serialize(() => {
@ -34,13 +34,12 @@ export const insertAccount = (
}
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) {
reject(err)
}
db.run('COMMIT')
resolve({
id,
username,
@ -164,22 +163,30 @@ FROM accounts INNER JOIN servers ON servers.account_id = accounts.id WHERE accou
export const removeAccount = (db: sqlite3.Database, id: number): Promise<null> => {
return new Promise((resolve, reject) => {
db.run('DELETE FROM accounts WHERE id = ?', id, err => {
if (err) {
reject(err)
}
resolve(null)
db.serialize(() => {
db.run('PRAGMA foreign_keys = ON')
db.run('DELETE FROM accounts WHERE id = ?', id, err => {
if (err) {
reject(err)
}
resolve(null)
})
})
})
}
export const removeAllAccounts = (db: sqlite3.Database): Promise<null> => {
return new Promise((resolve, reject) => {
db.run('DELETE FROM accounts', err => {
if (err) {
reject(err)
}
resolve(null)
db.serialize(() => {
db.run('PRAGMA foreign_keys = ON')
db.run('DELETE FROM accounts', err => {
if (err) {
reject(err)
}
resolve(null)
})
})
})
}

View File

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

View File

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

View File

@ -24,7 +24,7 @@
<FailoverImg :src="`${server.baseURL}/favicon.ico`" :failoverSrc="`${server.baseURL}/favicon.png`" class="instance-icon" />
<span>{{ server.domain }}</span>
</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" />
<span>New</span>
</el-menu-item>
@ -71,7 +71,7 @@ export default defineComponent({
}
})
.catch(_ => {
return router.push({ path: '/login' })
return router.push({ path: '/login/form' })
})
}

View File

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

View File

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

View File

@ -40,25 +40,28 @@
import { defineComponent, computed, reactive, ref } from 'vue'
import { useI18next } from 'vue3-i18next'
import { ElLoading, ElMessage, FormInstance, FormRules } from 'element-plus'
import { useStore } from '@/store'
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({
name: 'login-form',
setup() {
const space = 'Login'
const store = useStore()
const i18n = useI18next()
const router = useRouter()
const win = (window as any) as MyWindow
const form = reactive({
domainName: ''
})
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>({
domainName: [
{
@ -81,8 +84,20 @@ export default defineComponent({
background: 'rgba(0, 0, 0, 0.7)'
})
try {
await store.dispatch(`${space}/${ACTION_TYPES.ADD_SERVER}`)
await store.dispatch(`${space}/${ACTION_TYPES.ADD_APP}`)
const server: LocalServer = await win.ipcRenderer.invoke('add-server', domain.value)
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) {
ElMessage({
message: i18n.t('message.authorize_url_error'),
@ -96,27 +111,31 @@ export default defineComponent({
const confirm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(valid => {
await formEl.validate(async valid => {
if (valid) {
return store
.dispatch(`${space}/${ACTION_TYPES.CONFIRM_INSTANCE}`, form.domainName)
.then(() => {
ElMessage({
message: i18n.t('message.domain_confirmed', {
domain: form.domainName
}),
type: 'success'
})
searching.value = true
try {
const cleanDomain = form.domainName.trim()
sns.value = await detector(`https://${cleanDomain}`)
domain.value = cleanDomain
ElMessage({
message: i18n.t('message.domain_confirmed', {
domain: cleanDomain
}),
type: 'success'
})
.catch(err => {
ElMessage({
message: i18n.t('message.domain_doesnt_exist', {
domain: form.domainName
}),
type: 'error'
})
console.error(err)
} catch (err) {
console.error(err)
ElMessage({
message: i18n.t('message.domain_doesnt_exist', {
domain: form.domainName
}),
type: 'error'
})
} finally {
searching.value = false
}
return true
} else {
ElMessage({
message: i18n.t('validation.login.domain_format'),

View File

@ -153,7 +153,7 @@ export default defineComponent({
const removeAllAssociations = () => {
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 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 PreferencesGeneral from '@/components/Preferences/General.vue'
import PreferencesAppearance from '@/components/Preferences/Appearance.vue'
@ -34,9 +36,21 @@ import TimelineSpaceContentsBookmarks from '@/components/TimelineSpace/Contents/
const routes = [
{
path: '/login',
path: '/login/',
name: 'login',
component: Login
component: Login,
children: [
{
path: 'form',
name: 'login-form',
component: LoginForm
},
{
path: 'authorize',
name: 'authorize',
component: Authorize
}
]
},
{
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 GlobalHeader, { GlobalHeaderState } from './GlobalHeader'
import Login, { LoginState } from './Login'
import TimelineSpace, { TimelineSpaceModuleState } from './TimelineSpace'
import Preferences, { PreferencesModuleState } from './Preferences'
import Settings, { SettingsModuleState } from './Settings'
@ -15,7 +14,6 @@ const win = (window as any) as MyWindow
export interface RootState {
App: AppState
GlobalHeader: GlobalHeaderState
Login: LoginState
TimelineSpace: TimelineSpaceModuleState
Preferences: PreferencesModuleState
Settings: SettingsModuleState
@ -34,7 +32,6 @@ export default createStore({
modules: {
App,
GlobalHeader,
Login,
TimelineSpace,
Preferences,
Settings