mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
add claim support
This commit is contained in:
parent
584418eb8a
commit
7a4ecb14f7
6 changed files with 304 additions and 41 deletions
38
next-ui/src/colada/claim.ts
Normal file
38
next-ui/src/colada/claim.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { defineMutation, defineQuery, useMutation, useQuery, useQueryCache } from '@pinia/colada'
|
||||
import { komgaClient } from '@/api/komga-client'
|
||||
|
||||
export const QUERY_KEYS_CLAIM = {
|
||||
root: ['claim'] as const,
|
||||
}
|
||||
|
||||
export const useClaimStatus = defineQuery(() => {
|
||||
return useQuery({
|
||||
key: () => QUERY_KEYS_CLAIM.root,
|
||||
query: () =>
|
||||
komgaClient
|
||||
.GET('/api/v1/claim')
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
// forever
|
||||
staleTime: 0,
|
||||
gcTime: false,
|
||||
})
|
||||
})
|
||||
|
||||
export const useClaimServer = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: ({ username, password }: { username: string; password: string }) =>
|
||||
komgaClient.POST('/api/v1/claim', {
|
||||
params: {
|
||||
header: {
|
||||
'X-Komga-Email': username,
|
||||
'X-Komga-Password': password,
|
||||
},
|
||||
},
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryCache.cancelQueries({ key: QUERY_KEYS_CLAIM.root })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -52,6 +52,36 @@ export const useCurrentUser = defineQuery(() => {
|
|||
}
|
||||
})
|
||||
|
||||
export const useLogin = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: ({
|
||||
username,
|
||||
password,
|
||||
rememberMe,
|
||||
}: {
|
||||
username: string
|
||||
password: string
|
||||
rememberMe?: boolean
|
||||
}) =>
|
||||
komgaClient.GET('/api/v2/users/me', {
|
||||
headers: {
|
||||
authorization: 'Basic ' + btoa(username + ':' + password),
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
params: {
|
||||
query: {
|
||||
'remember-me': rememberMe,
|
||||
},
|
||||
},
|
||||
}),
|
||||
onSuccess: ({ data }) => {
|
||||
queryCache.setQueryData(QUERY_KEYS_USERS.currentUser, data)
|
||||
queryCache.cancelQueries({ key: QUERY_KEYS_USERS.currentUser })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const useLogout = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
|
|
|
|||
192
next-ui/src/pages/claim.vue
Normal file
192
next-ui/src/pages/claim.vue
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
:disabled="isLoading"
|
||||
@submit.prevent="submitForm()"
|
||||
>
|
||||
<v-container max-width="400px">
|
||||
<v-row justify="center">
|
||||
<v-col
|
||||
cols="7"
|
||||
sm="10"
|
||||
>
|
||||
<v-img src="@/assets/logo.svg" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-alert
|
||||
color="info"
|
||||
variant="tonal"
|
||||
icon="i-mdi:info"
|
||||
:text="
|
||||
$formatMessage({
|
||||
description: 'Claim server screen: information banner',
|
||||
defaultMessage:
|
||||
'This Komga server is not yet active, you need to create a user account to be able to access it.',
|
||||
id: '2p+JVw',
|
||||
})
|
||||
"
|
||||
>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Claim server screen: email field label',
|
||||
defaultMessage: 'Email',
|
||||
id: 'aDFZOW',
|
||||
})
|
||||
"
|
||||
autofocus
|
||||
:rules="['required', 'email']"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Claim server screen: password field label',
|
||||
defaultMessage: 'Password',
|
||||
id: 'U3Uo3q',
|
||||
})
|
||||
"
|
||||
:rules="['required']"
|
||||
autocomplete="off"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-inner-icon="showPassword ? 'i-mdi:eye' : 'i-mdi:eye-off'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
:label="
|
||||
$formatMessage({
|
||||
description: 'Claim server screen: Confirm password field label',
|
||||
defaultMessage: 'Confirm password',
|
||||
id: 'ezgnXr',
|
||||
})
|
||||
"
|
||||
:rules="[
|
||||
[
|
||||
'sameAs',
|
||||
password,
|
||||
$formatMessage({
|
||||
description: 'Claim server screen:: Error message if passwords differ',
|
||||
defaultMessage: 'Passwords must be identical',
|
||||
id: 'ZWFPAg',
|
||||
}),
|
||||
],
|
||||
]"
|
||||
autocomplete="off"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-inner-icon="showPassword ? 'i-mdi:eye' : 'i-mdi:eye-off'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn
|
||||
:text="
|
||||
$formatMessage({
|
||||
description: 'Claim server screen:: Create User Account button',
|
||||
defaultMessage: 'Create User Account',
|
||||
id: 'd8lMZe',
|
||||
})
|
||||
"
|
||||
:loading="isLoading"
|
||||
type="submit"
|
||||
block
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-row justify="center">
|
||||
<v-col cols="auto">
|
||||
<div class="d-flex ga-4">
|
||||
<FragmentLocaleSelector />
|
||||
<FragmentThemeSelector />
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ErrorCause } from '@/api/komga-client'
|
||||
import { useMessagesStore } from '@/stores/messages'
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
import { useClaimServer, useClaimStatus } from '@/colada/claim'
|
||||
import { useLogin } from '@/colada/users'
|
||||
|
||||
const messagesStore = useMessagesStore()
|
||||
const intl = useIntl()
|
||||
|
||||
const form = ref()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const showPassword = ref<boolean>(false)
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const { mutateAsync: login, isLoading: isLoadingLogin } = useLogin()
|
||||
const { mutateAsync: claimServer, isLoading: isLoadingClaim } = useClaimServer()
|
||||
|
||||
const isLoading = computed(() => isLoadingClaim.value || isLoadingLogin.value)
|
||||
|
||||
async function submitForm() {
|
||||
const { valid } = await form.value.validate()
|
||||
if (valid) {
|
||||
const credentials = {
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
}
|
||||
|
||||
claimServer(credentials)
|
||||
.then(() => {
|
||||
void login(credentials).then(() => {
|
||||
if (route.query.redirect) void router.push({ path: route.query.redirect.toString() })
|
||||
else void router.push('/')
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message ||
|
||||
intl.formatMessage(commonMessages.networkError),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { refresh: claimStatus } = useClaimStatus()
|
||||
void claimStatus().then(({ data }) => {
|
||||
if (data?.isClaimed) void router.push('/')
|
||||
})
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: single
|
||||
noAuth: true
|
||||
</route>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<v-form
|
||||
v-model="formValid"
|
||||
ref="form"
|
||||
:disabled="isLoading"
|
||||
@submit.prevent="submitForm()"
|
||||
>
|
||||
|
|
@ -116,18 +116,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ErrorCause, komgaClient } from '@/api/komga-client'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import { type ErrorCause } from '@/api/komga-client'
|
||||
import { useMessagesStore } from '@/stores/messages'
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useLogin } from '@/colada/users'
|
||||
import { useClaimStatus } from '@/colada/claim'
|
||||
|
||||
const messagesStore = useMessagesStore()
|
||||
const intl = useIntl()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const formValid = ref<boolean>(false)
|
||||
const form = ref()
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loginError = ref<string>('')
|
||||
|
|
@ -135,44 +136,40 @@ const loginError = ref<string>('')
|
|||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
const { mutate: performLogin, isLoading } = useMutation({
|
||||
mutation: () =>
|
||||
komgaClient.GET('/api/v2/users/me', {
|
||||
headers: {
|
||||
authorization: 'Basic ' + btoa(username.value + ':' + password.value),
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
params: {
|
||||
query: {
|
||||
'remember-me': appStore.rememberMe,
|
||||
},
|
||||
},
|
||||
}),
|
||||
onSuccess: ({ data }) => {
|
||||
queryCache.setQueryData(['current-user'], data)
|
||||
queryCache.cancelQueries({ key: ['current-user'] })
|
||||
if (route.query.redirect) void router.push({ path: route.query.redirect.toString() })
|
||||
else void router.push('/')
|
||||
},
|
||||
onError: (error) => {
|
||||
if ((error?.cause as ErrorCause)?.status === 401)
|
||||
loginError.value = intl.formatMessage({
|
||||
description: 'Login screen: error message displayed when login failed',
|
||||
defaultMessage: 'Invalid login or password',
|
||||
id: 'AjWlka',
|
||||
})
|
||||
else
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message || intl.formatMessage(commonMessages.networkError),
|
||||
})
|
||||
},
|
||||
})
|
||||
const { mutateAsync: performLogin, isLoading } = useLogin()
|
||||
|
||||
function submitForm() {
|
||||
if (formValid.value) performLogin()
|
||||
async function submitForm() {
|
||||
const { valid } = await form.value.validate()
|
||||
if (valid)
|
||||
performLogin({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
rememberMe: appStore.rememberMe,
|
||||
})
|
||||
.then(() => {
|
||||
if (route.query.redirect) void router.push({ path: route.query.redirect.toString() })
|
||||
else void router.push('/')
|
||||
})
|
||||
.catch((error) => {
|
||||
if ((error?.cause as ErrorCause)?.status === 401)
|
||||
loginError.value = intl.formatMessage({
|
||||
description: 'Login screen: error message displayed when login failed',
|
||||
defaultMessage: 'Invalid login or password',
|
||||
id: 'AjWlka',
|
||||
})
|
||||
else
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message ||
|
||||
intl.formatMessage(commonMessages.networkError),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const { refresh: claimStatus } = useClaimStatus()
|
||||
void claimStatus().then(({ data }) => {
|
||||
if (data?.isClaimed == false) void router.push('/')
|
||||
})
|
||||
</script>
|
||||
|
||||
<route lang="yaml">
|
||||
|
|
|
|||
|
|
@ -10,19 +10,24 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { useCurrentUser } from '@/colada/users'
|
||||
import { useClaimStatus } from '@/colada/claim'
|
||||
|
||||
async function checkAuthenticated() {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { data, error, refresh } = useCurrentUser()
|
||||
const { data: claimData, refresh: claimRefresh } = useClaimStatus()
|
||||
|
||||
await refresh()
|
||||
await claimRefresh()
|
||||
if (data.value) {
|
||||
if (route.query.redirect) await router.push({ path: route.query.redirect.toString() })
|
||||
else await router.push('/')
|
||||
}
|
||||
if (error.value) {
|
||||
await router.push({ name: '/login', query: { redirect: route.query.redirect } })
|
||||
if (claimData.value?.isClaimed)
|
||||
await router.push({ name: '/login', query: { redirect: route.query.redirect } })
|
||||
else await router.push({ name: '/claim', query: { redirect: route.query.redirect } })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
1
next-ui/src/typed-router.d.ts
vendored
1
next-ui/src/typed-router.d.ts
vendored
|
|
@ -23,6 +23,7 @@ declare module 'vue-router/auto-routes' {
|
|||
'/account/api-keys': RouteRecordInfo<'/account/api-keys', '/account/api-keys', Record<never, never>, Record<never, never>>,
|
||||
'/account/details': RouteRecordInfo<'/account/details', '/account/details', Record<never, never>, Record<never, never>>,
|
||||
'/account/ui': RouteRecordInfo<'/account/ui', '/account/ui', Record<never, never>, Record<never, never>>,
|
||||
'/claim': RouteRecordInfo<'/claim', '/claim', Record<never, never>, Record<never, never>>,
|
||||
'/history': RouteRecordInfo<'/history', '/history', Record<never, never>, Record<never, never>>,
|
||||
'/import/books': RouteRecordInfo<'/import/books', '/import/books', Record<never, never>, Record<never, never>>,
|
||||
'/import/readlist': RouteRecordInfo<'/import/readlist', '/import/readlist', Record<never, never>, Record<never, never>>,
|
||||
|
|
|
|||
Loading…
Reference in a new issue