Skip to content

Commit

Permalink
Merge pull request #46 from doroudi:doroudi/issue8
Browse files Browse the repository at this point in the history
Doroudi/issue8
  • Loading branch information
doroudi authored Jan 3, 2024
2 parents 039c7b1 + 3bf6ce1 commit 5da9e48
Show file tree
Hide file tree
Showing 17 changed files with 185 additions and 62 deletions.
6 changes: 5 additions & 1 deletion locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ userMenu:
language: Language
theme: Theme
options: Options
signOut: SignOut
logout: Logout
dark: Dark
light: Light
en: English
Expand All @@ -54,6 +54,10 @@ login:
haveNotAccount: Don't have an account?
createAccount: Create One!
failedMessage: Login Failed
validations:
userNameRequired: Enter username
passwordRequired: Password is required

register:
title: Register
username: User Name
Expand Down
4 changes: 2 additions & 2 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ watch(() => layout.activeLanguage, () => {

<template>
<n-config-provider :theme-overrides="themeOverrides" :rtl="layout.isRtl ? rtlStyles : []" :preflight-style-disabled="false">
<n-notification-provider placement="bottom">
<n-message-provider placement="bottom">
<n-notification-provider placement="bottom-right">
<n-message-provider placement="bottom-right">
<n-dialog-provider>
<router-view />
</n-dialog-provider>
Expand Down
9 changes: 2 additions & 7 deletions src/common/api/api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,8 @@ export class ApiService {
}

async post<T>(url: string, data: any): Promise<T> {
try {
return this.httpClient.post(`${this.apiBase}/${url}`, data)
}
catch (error) {
console.error(`${error} was occurred`)
throw new Error('cannot post')
}
return this.httpClient.post<T>(`${this.apiBase}/${url}`, data)

Check failure on line 39 in src/common/api/api-service.ts

View workflow job for this annotation

GitHub Actions / typecheck

Type 'AxiosResponse<T, any>' is not assignable to type 'T'.
return response.data

Check failure on line 40 in src/common/api/api-service.ts

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find name 'response'.
}

async put<T>(url: string, data: any): Promise<T> {
Expand Down
9 changes: 9 additions & 0 deletions src/common/api/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ function httpClient(baseApi: string | null = null): AxiosInstance {
return Promise.reject(error)
},
)

client.interceptors.response.use((response) => {
return response
}, (error) => {
if (error.response.statusText)
useNotifyStore().error(error.response.statusText)

return Promise.reject(error)
})
return client
}

Expand Down
2 changes: 2 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ declare module 'vue' {
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
Expand Down
10 changes: 9 additions & 1 deletion src/components/UserProfile.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
<script setup lang="ts">
import { ChevronCircleDown20Regular as ChevronIcon } from '@vicons/fluent'
const accountStore = useAccountStore()
const { t } = useI18n()
const selectedItem = ref('')
const router = useRouter()
const items
= [
{ label: t('userMenu.options'), to: '/options', value: () => { router.push('/options') } },
{ label: t('userMenu.signOut'), to: '/logout', value: () => { router.push('/account/login') } },
{
label: t('userMenu.logout'),
to: '/logout',
value: () => {
accountStore.logout()
router.push('/account/login')
},
},
]
function doMenuAction(value: any) {
Expand Down
18 changes: 18 additions & 0 deletions src/layouts/auth.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
<script setup lang="ts">
import type { ToastNotification } from '~/store/notify.store'
const notification = useNotification()
const notificationsStore = useNotifyStore()
watch(() => notificationsStore.messages, (newVal: ToastNotification[], oldVal: ToastNotification[]) => {
if (newVal.length < oldVal.length)
return
const lastMessage = newVal[newVal.length - 1]
notification.create({
type: lastMessage.type,
content: lastMessage.body,
duration: !lastMessage.permanent ? lastMessage.duration : undefined,
closable: !lastMessage.permanent,
})
}, { deep: true })
</script>

<template>
<main class="text-gray-700 dark:text-gray-200">
<RouterView />
Expand Down
13 changes: 9 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,20 @@ app.use(router)
Object.values(import.meta.glob<{ install: AppModule }>('./modules/*.ts', { eager: true }))
.forEach(i => i.install?.(app, router))

// @ts-expect-error "Type instantiation is excessively deep and possibly infinite.ts(2589)"
const { t } = i18n.global
let title = t('title')

router.beforeEach((to, from, next) => {
// @ts-expect-error "Type instantiation is excessively deep and possibly infinite.ts(2589)"
const { t } = i18n.global
let title = t('title')
if (to.meta.title)
title = `${to.meta.title} - ${title}`

document.title = title

const isAuthenticated = useAccountStore().isAuthenticated()
const isAuthRequired = to.meta.authRequired ?? true
if (isAuthRequired && !isAuthenticated)
next({ path: '/account/login' })

next()
})
enableMocking().then(() => app.mount('#app'))
18 changes: 18 additions & 0 deletions src/mocks/handlers/account.handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { HttpResponse, delay, http } from 'msw'
import type { LoginResponse, LoginViewModel } from '~/models/Login'

const handlers = [
http.post('/api/account/login', async ({ request }) => {
const user = (await request.json()) as LoginViewModel
if (user.username === 'admin' && user.password === 'admin') {
const response: LoginResponse = { token: 'JWT_Fake_Token', isSucceed: true }
await delay(1000)
return HttpResponse.json(response, { status: 200 })
}
else {
return HttpResponse.json(null, { status: 400, statusText: 'UserName or Password is not correct' })
}
}),
]

export default handlers
14 changes: 14 additions & 0 deletions src/models/Login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface LoginViewModel {
username: string
password: string
}

export interface LoginResponse {
token: string
isSucceed: boolean
}

export interface Account {
username?: string
token: string
}
4 changes: 0 additions & 4 deletions src/models/LoginInfo.ts

This file was deleted.

72 changes: 52 additions & 20 deletions src/pages/Account/login.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
<script setup lang="ts">
import type { FormInst, FormRules } from 'naive-ui/es/form/src/interface'
import { storeToRefs } from 'pinia'
import type { LoginViewModel } from '~/models/Login'
const { t } = useI18n()
const accountStore = useAccountStore()
const { isLoading, loginFailed } = storeToRefs(accountStore)
const { isLoading } = storeToRefs(accountStore)
const loginInfo = ref<LoginViewModel>({ username: '', password: '' })
const loginFailed = ref(false)
const router = useRouter()
const formRef = ref<FormInst | null>(null)
async function login() {
accountStore.login()
formRef.value?.validate(async (errors: any) => {
if (!errors) {
const loginSucceed = await accountStore.login(loginInfo.value)
if (loginSucceed) {
useNotifyStore().success('Logged In Successfully 🎉')
setTimeout(() => router.push('/'), 500)
}
else {
loginFailed.value = true
setTimeout(() => {
loginFailed.value = false
}, 2000)
}
}
})
}
const rules: FormRules = {
username: [
{
required: true,
trigger: ['blur', 'change'],
message: t('login.validations.userNameRequired'),
},
],
password: [
{
required: true,
trigger: ['blur', 'change'],
message: t('login.validations.passwordRequired'),
},
],
}
</script>

<route lang="yaml">
meta:
title: Login
layout: auth
authRequired: false
</route>

<template>
Expand All @@ -22,22 +60,20 @@ meta:
<div class="shadow-lg bg-white dark:bg-slate-800 rounded-md w-full" :class="{ failed: loginFailed }">
<div class="banner" />
<div class="p-5">
<!-- <img src="@/assets/images/login.jpg" alt="Image" height="50" class="mb-3"> -->
<div class="text-2xl font-medium mb-8">
{{ t('login.title') }}
</div>
<form @submit.prevent="login()">
<div class="mb-5">
<span class="p-float-label">
<label for="username" class="block font-medium">{{ t('login.username') }}</label>
<n-input id="username" />
</span>
</div>
<n-form ref="formRef" :model="loginInfo" :rules="rules" @submit.prevent="login()">
<n-form-item class="mb-5" path="username" :label="t('login.username')">
<n-input id="name" v-model:value="loginInfo.username" autofocus :placeholder="t('login.username')" />
</n-form-item>
<n-form-item class="mb-5" path="password" :label="t('login.password')">
<n-input
id="name" v-model:value="loginInfo.password" type="password" show-password-on="mousedown"
:placeholder="t('login.password')"
/>
</n-form-item>

<div class="mb-8">
<label for="password" class="block font-medium">{{ t('login.password') }}</label>
<n-input type="password" show-password-on="mousedown" />
</div>
<div class="flex align-items-center justify-between mb-10">
<!-- <div class="flex align-items-center">
<n-checkbox v-model="checked" aria-labelledby="remember" :binary="true" class="mr-2" />
Expand All @@ -54,7 +90,7 @@ meta:
<n-button attr-type="submit" size="large" :block="true" type="primary" :loading="isLoading">
{{ t('login.loginButton') }}
</n-button>
</form>
</n-form>
<div class="text-center pt-4 text-sm">
<span class="font-medium line-height-3">{{ t('login.haveNotAccount') }}</span>
<RouterLink to="/Account/Register" class="font-medium no-underline ml-2 text-blue-500 cursor-pointer">
Expand All @@ -71,10 +107,6 @@ meta:
</template>

<style lang='scss'>
// .bg {
// background: #f2f2f2; // linear-gradient(15deg, #006EB8 50%, #4795D1 50.1%);
// }
.banner {
background-image: url('~/assets/images/login_banner.jpg');
background-size: cover;
Expand All @@ -86,7 +118,7 @@ meta:
.login-box {
max-width: 400px;
&.failed {
.failed {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
Expand Down
5 changes: 5 additions & 0 deletions src/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const { t } = useI18n()
// const { options } = useOptions()
</script>

<route lang="yaml">
meta:
title: Home
</route>

<template>
<div class="margin-outside flex pb-3">
<DashboardCard class="w-1/5" :icon="UserIcon" title="Registers" :progress="10" :value="250" />
Expand Down
12 changes: 12 additions & 0 deletions src/services/account.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiService } from '~/common/api/api-service'
import type { LoginResponse, LoginViewModel } from '~/models/Login'

const apiService = new ApiService('account')
class AccountService {
async login(loginInfo: LoginViewModel): Promise<LoginResponse> {
const response = await apiService.post<any>('login', loginInfo)
return response.data
}
}

export default new AccountService()
7 changes: 0 additions & 7 deletions src/services/accout.service.ts

This file was deleted.

42 changes: 27 additions & 15 deletions src/store/account.store.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,48 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import type { LoginViewModel } from '~/models/LoginInfo'
import type { Account, LoginViewModel } from '~/models/Login'
import AccountService from '~/services/account.service'

export const useAccountStore = defineStore('account', () => {
const user = ref<LoginViewModel>({ username: '', password: '' })
const user = ref<Account | null>(null)
const isLoading = ref(false)
const loginFailed = ref(false)
function login() {
setTimeout(async () => {
if (user.value.username === 'admin' && user.value.password === 'admin') {
// const response = await authService.login(user.value.username, user.value.password)
isLoading.value = false
// toast.add({ severity: 'success', summary: 'Login Succeed', detail: 'Redirecting', life: 3000 })
return

async function login(loginInfo: LoginViewModel): Promise<boolean> {
isLoading.value = true
try {
const response = await AccountService.login(loginInfo)
if (response.isSucceed) {
user.value = {
token: response.token,
}
return true
}

return false
}
catch (error) {
return false
}
finally {
isLoading.value = false
loginFailed.value = true
// toast.add({ severity: 'error', summary: t('login.failedMessage'), detail: 'Error Message', life: 3000 })
setTimeout(() => loginFailed.value = false, 2000)
}, 1000)
}
// toast.add({ severity: 'error', summary: t('login.failedMessage'), detail: 'Error Message', life: 3000 })
}

function logout() {
// useRouter().push()
user.value = null
}

function isAuthenticated() {
return (user.value?.token && user.value.token !== null) ?? false
}

return {
isLoading,
loginFailed,
user,
login,
logout,
isAuthenticated,
}
})

Expand Down
Loading

0 comments on commit 5da9e48

Please sign in to comment.