add auth form, validation

This commit is contained in:
Sergey Krylov 2025-02-04 07:55:33 +03:00
parent eb5ddf9f2e
commit f51cc9cc4d
7 changed files with 255 additions and 10 deletions

View File

@ -9,8 +9,11 @@
"preview": "vite preview"
},
"dependencies": {
"@vee-validate/zod": "^4.15.0",
"vee-validate": "^4.15.0",
"vue": "3.5.13",
"vue-router": "4.5.0"
"vue-router": "4.5.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "5.2.1",

124
pnpm-lock.yaml generated
View File

@ -8,12 +8,21 @@ importers:
.:
dependencies:
'@vee-validate/zod':
specifier: ^4.15.0
version: 4.15.0(vue@3.5.13(typescript@5.7.3))(zod@3.24.1)
vee-validate:
specifier: ^4.15.0
version: 4.15.0(vue@3.5.13(typescript@5.7.3))
vue:
specifier: 3.5.13
version: 3.5.13(typescript@5.7.3)
vue-router:
specifier: 4.5.0
version: 4.5.0(vue@3.5.13(typescript@5.7.3))
zod:
specifier: ^3.24.1
version: 3.24.1
devDependencies:
'@vitejs/plugin-vue':
specifier: 5.2.1
@ -364,6 +373,11 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@vee-validate/zod@4.15.0':
resolution: {integrity: sha512-MpvIKiyg9X5yD8bJW0no2AU7wtR2T5mrvD9tuPRiie951sU2n6QKgMV38qKKOiqFBCxsMSjIuLLLV3V5kVE4nQ==}
peerDependencies:
zod: ^3.24.0
'@vitejs/plugin-vue@5.2.1':
resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==}
engines: {node: ^18.0.0 || >=20.0.0}
@ -398,6 +412,15 @@ packages:
'@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
'@vue/devtools-api@7.7.1':
resolution: {integrity: sha512-Cexc8GimowoDkJ6eNelOPdYIzsu2mgNyp0scOQ3tiaYSb9iok6LOESSsJvHaI+ib3joRfqRJNLkHFjhNuWA5dg==}
'@vue/devtools-kit@7.7.1':
resolution: {integrity: sha512-yhZ4NPnK/tmxGtLNQxmll90jIIXdb2jAhPF76anvn5M/UkZCiLJy28bYgPIACKZ7FCosyKoaope89/RsFJll1w==}
'@vue/devtools-shared@7.7.1':
resolution: {integrity: sha512-BtgF7kHq4BHG23Lezc/3W2UhK2ga7a8ohAIAGJMBr4BkxUFzhqntQtCiuL1ijo2ztWnmusymkirgqUrXoQKumA==}
'@vue/language-core@2.2.0':
resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==}
peerDependencies:
@ -460,6 +483,9 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
birpc@0.2.19:
resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==}
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@ -487,6 +513,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
copy-anything@3.0.5:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -635,6 +665,9 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@ -655,6 +688,10 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@ -698,6 +735,9 @@ packages:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -742,6 +782,9 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@ -765,6 +808,9 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rollup@4.34.1:
resolution: {integrity: sha512-iYZ/+PcdLYSGfH3S+dGahlW/RWmsqDhLgj1BT9DH/xXJ0ggZN7xkdP9wipPNjjNLczI+fmMLmTB9pye+d2r4GQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -787,10 +833,18 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
speakingurl@14.0.1:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
superjson@2.2.2:
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
engines: {node: '>=16'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -803,6 +857,10 @@ packages:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
type-fest@4.33.0:
resolution: {integrity: sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==}
engines: {node: '>=16'}
typescript@5.7.3:
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
engines: {node: '>=14.17'}
@ -814,6 +872,11 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vee-validate@4.15.0:
resolution: {integrity: sha512-PGJh1QCFwCBjbHu5aN6vB8macYVWrajbDvgo1Y/8fz9n/RVIkLmZCJDpUgu7+mUmCOPMxeyq7vXUOhbwAqdXcA==}
peerDependencies:
vue: ^3.4.26
vite@6.0.11:
resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -899,6 +962,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zod@3.24.1:
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
snapshots:
'@babel/helper-string-parser@7.25.9': {}
@ -1107,6 +1173,14 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@vee-validate/zod@4.15.0(vue@3.5.13(typescript@5.7.3))(zod@3.24.1)':
dependencies:
type-fest: 4.33.0
vee-validate: 4.15.0(vue@3.5.13(typescript@5.7.3))
zod: 3.24.1
transitivePeerDependencies:
- vue
'@vitejs/plugin-vue@5.2.1(vite@6.0.11)(vue@3.5.13(typescript@5.7.3))':
dependencies:
vite: 6.0.11
@ -1161,6 +1235,24 @@ snapshots:
'@vue/devtools-api@6.6.4': {}
'@vue/devtools-api@7.7.1':
dependencies:
'@vue/devtools-kit': 7.7.1
'@vue/devtools-kit@7.7.1':
dependencies:
'@vue/devtools-shared': 7.7.1
birpc: 0.2.19
hookable: 5.5.3
mitt: 3.0.1
perfect-debounce: 1.0.0
speakingurl: 14.0.1
superjson: 2.2.2
'@vue/devtools-shared@7.7.1':
dependencies:
rfdc: 1.4.1
'@vue/language-core@2.2.0(typescript@5.7.3)':
dependencies:
'@volar/language-core': 2.4.11
@ -1226,6 +1318,8 @@ snapshots:
balanced-match@1.0.2: {}
birpc@0.2.19: {}
boolbase@1.0.0: {}
brace-expansion@1.1.11:
@ -1252,6 +1346,10 @@ snapshots:
concat-map@0.0.1: {}
copy-anything@3.0.5:
dependencies:
is-what: 4.1.16
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -1434,6 +1532,8 @@ snapshots:
he@1.2.0: {}
hookable@5.5.3: {}
ignore@5.3.2: {}
import-fresh@3.3.1:
@ -1449,6 +1549,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
is-what@4.1.16: {}
isexe@2.0.0: {}
js-yaml@4.1.0:
@ -1490,6 +1592,8 @@ snapshots:
dependencies:
brace-expansion: 2.0.1
mitt@3.0.1: {}
ms@2.1.3: {}
muggle-string@0.4.1: {}
@ -1529,6 +1633,8 @@ snapshots:
path-key@3.1.1: {}
perfect-debounce@1.0.0: {}
picocolors@1.1.1: {}
postcss-selector-parser@6.1.2:
@ -1548,6 +1654,8 @@ snapshots:
resolve-from@4.0.0: {}
rfdc@1.4.1: {}
rollup@4.34.1:
dependencies:
'@types/estree': 1.0.6
@ -1583,8 +1691,14 @@ snapshots:
source-map-js@1.2.1: {}
speakingurl@14.0.1: {}
strip-json-comments@3.1.1: {}
superjson@2.2.2:
dependencies:
copy-anything: 3.0.5
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@ -1595,6 +1709,8 @@ snapshots:
type-fest@0.20.2: {}
type-fest@4.33.0: {}
typescript@5.7.3: {}
uri-js@4.4.1:
@ -1603,6 +1719,12 @@ snapshots:
util-deprecate@1.0.2: {}
vee-validate@4.15.0(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@vue/devtools-api': 7.7.1
type-fest: 4.33.0
vue: 3.5.13(typescript@5.7.3)
vite@6.0.11:
dependencies:
esbuild: 0.24.2
@ -1656,3 +1778,5 @@ snapshots:
xml-name-validator@4.0.0: {}
yocto-queue@0.1.0: {}
zod@3.24.1: {}

View File

@ -25,7 +25,6 @@
:is="layout + '-layout'"
v-if="layout"
/>
<RouterView />
</template>
<style scoped>

57
src/hooks/useLoginForm.ts Normal file
View File

@ -0,0 +1,57 @@
import * as z from 'zod';
import { useField, useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { computed, watch } from 'vue';
const useLoginForm = () => {
const schema = z.object({
email: z
.string()
.trim()
.min(1, 'Обязательное поле')
.email('Невалидный email')
.default(''),
password: z
.string()
.trim()
.min(1, 'Обязательное поле')
.min(6, 'Должно содержать больше 6 символов')
.default('')
})
const {handleSubmit, isSubmitting, submitCount} = useForm({
validationSchema: toTypedSchema(schema)
});
const onSubmit = handleSubmit((values) => {
console.log(values);
})
const {value: email, errorMessage: eError, handleBlur: eBlur} = useField('email');
const {value: password, errorMessage: pError, handleBlur: pBlur} = useField('password');
const isTooManyAttempts = computed(() => submitCount.value > 2);
const disabled = computed(() => isSubmitting.value || isTooManyAttempts.value)
watch(isTooManyAttempts, (value) => {
if (value) {
setTimeout(() => {
submitCount.value = 0;
}, 2000)
}
})
return {
email,
eError,
eBlur,
password,
pError,
pBlur,
disabled,
onSubmit,
isTooManyAttempts,
}
};
export default useLoginForm;

View File

@ -1,6 +1,8 @@
<template>
<div class="container">
<div class="card" />
<div class="card">
<router-view />
</div>
</div>
</template>

View File

@ -1,7 +1,9 @@
<template>
<the-navbar />
<div class="container with-nav">
<div class="card" />
<div class="card">
<router-view />
</div>
</div>
</template>

View File

@ -1,13 +1,71 @@
<template>
<div>
<h1>Auth</h1>
</div>
<form
class="card"
@submit.prevent="onSubmit"
>
<h1>Войти в систему</h1>
<div
class="form-control"
:class="{invalid: eError}"
>
<label for="email">E-mail</label>
<input
id="email"
v-model="email"
type="email"
:disabled="disabled"
@blur="eBlur"
>
<small v-if="eError">{{ eError }}</small>
</div>
<div
class="form-control"
:class="{invalid: pError}"
>
<label for="password">Пароль</label>
<input
id="password"
v-model="password"
type="password"
:disabled="disabled"
@blur="pBlur"
>
<small v-if="pError">{{ pError }}</small>
</div>
<button
class="btn primary"
type="submit"
:disabled="disabled"
>
Войти
</button>
<div
v-if="isTooManyAttempts"
class="text-danger"
>
Слишком частые попытки войти в систему
</div>
</form>
</template>
<script>
export default {
<script setup>
import useLoginForm from '../hooks/useLoginForm.ts';
}
const {
disabled,
eBlur,
eError,
email,
onSubmit,
password,
pBlur,
pError,
isTooManyAttempts
} = useLoginForm();
</script>
<style lang="scss" scoped>