Browse Source

update: login page

pull/1397/head
tetuaoro 7 months ago
committed by Bernd Storath
parent
commit
53ad547dd0
  1. 2
      src/app/components/error/Toast.vue
  2. 2
      src/app/components/panel/Panel.vue
  3. 41
      src/app/pages/login.vue
  4. 2
      src/app/pages/me.vue
  5. 132
      src/app/pages/setup.vue
  6. 4
      src/app/stores/auth.ts
  7. 4
      src/app/utils/api.ts
  8. 4
      src/locales/en.json
  9. 2
      src/server/api/account/setup.post.ts
  10. 2
      src/server/api/session.post.ts
  11. 13
      src/server/utils/types.ts

2
src/app/components/error/Toast.vue

@ -7,7 +7,7 @@ const props = defineProps<{
const open = ref(true); const open = ref(true);
const autoCloseToast = props.duration ? Number(props.duration) : 5000; // 5 seconds const autoCloseToast = props.duration ? Number(props.duration) : 12000; // 12 seconds
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {

2
src/app/components/panel/Panel.vue

@ -1,7 +1,7 @@
<template> <template>
<div class="container mx-auto max-w-3xl px-3 md:px-0"> <div class="container mx-auto max-w-3xl px-3 md:px-0">
<div <div
class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden" class="shadow-md rounded-lg bg-white dark:bg-neutral-700 text-gray-700 dark:text-neutral-200 overflow-hidden"
> >
<slot /> <slot />
</div> </div>

41
src/app/pages/login.vue

@ -1,8 +1,6 @@
<template> <template>
<section> <section class="text-gray-700 dark:text-neutral-200">
<h1 <h1 class="text-4xl font-medium my-16 text-center">
class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center"
>
<img src="/logo.png" width="32" class="inline align-middle dark:bg" /> <img src="/logo.png" width="32" class="inline align-middle dark:bg" />
<span class="align-middle">WireGuard</span> <span class="align-middle">WireGuard</span>
</h1> </h1>
@ -79,16 +77,41 @@
:value="$t('signIn')" :value="$t('signIn')"
/> />
</form> </form>
<ErrorToast
v-if="setupError"
:title="setupError.title"
:message="setupError.message"
/>
</section> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { FetchError } from 'ofetch';
const { t } = useI18n();
const authenticating = ref(false); const authenticating = ref(false);
const remember = ref(false); const remember = ref(false);
const username = ref<null | string>(null); const username = ref<null | string>(null);
const password = ref<null | string>(null); const password = ref<null | string>(null);
const authStore = useAuthStore(); const authStore = useAuthStore();
type SetupError = {
title: string;
message: string;
};
const setupError = ref<null | SetupError>(null);
watch(setupError, (value) => {
if (value) {
setTimeout(() => {
setupError.value = null;
}, 13000);
}
});
async function login(e: Event) { async function login(e: Event) {
e.preventDefault(); e.preventDefault();
@ -104,10 +127,12 @@ async function login(e: Event) {
if (res) { if (res) {
await navigateTo('/'); await navigateTo('/');
} }
} catch (err) { } catch (error) {
if (err instanceof Error) { if (error instanceof FetchError) {
// TODO: replace alert with actual ui error message setupError.value = {
alert(err.message || err.toString()); title: t('error.login'),
message: error.data.message,
};
} }
} }
authenticating.value = false; authenticating.value = false;

2
src/app/pages/me.vue

@ -108,6 +108,8 @@ const username = ref(authStore.userData?.username);
const name = ref(authStore.userData?.name); const name = ref(authStore.userData?.name);
const email = ref(authStore.userData?.email); const email = ref(authStore.userData?.email);
// TODO: handle update password
const currentPassword = ref(authStore.userData?.email); const currentPassword = ref(authStore.userData?.email);
const newPassword = ref(authStore.userData?.email); const newPassword = ref(authStore.userData?.email);
const confirmPassword = ref(authStore.userData?.email); const confirmPassword = ref(authStore.userData?.email);

132
src/app/pages/setup.vue

@ -1,77 +1,75 @@
<template> <template>
<main class="container mx-auto px-4"> <main class="container mx-auto px-4">
<h1 <h1
class="text-4xl font-medium my-16 text-gray-700 dark:text-neutral-200 text-center" class="text-4xl font-medium my-16 text-center text-gray-700 dark:text-neutral-200"
> >
<img src="/logo.png" width="32" class="inline align-middle dark:bg" /> <img src="/logo.png" width="32" class="inline align-middle dark:bg" />
<span class="align-middle">WireGuard</span> <span class="align-middle">WireGuard</span>
</h1> </h1>
<div <Panel>
class="flex flex-col items-center lg:w-[60%] mx-auto shadow rounded-md bg-white dark:bg-neutral-700 p-5 overflow-hidden mt-10 text-gray-700 dark:text-neutral-200" <PanelBody class="lg:w-[60%] mx-auto mt-10 p-4">
> <h2 class="mt-8 mb-16 text-3xl font-medium">
<h2 {{ $t('setup.welcome') }}
class="mt-8 mb-16 text-3xl font-medium text-gray-700 dark:text-neutral-200" </h2>
> <p class="text-lg p-8">{{ $t('setup.msg') }}</p>
{{ $t('setup.welcome') }} <form class="mb-8" @submit.prevent="newAccount">
</h2> <div>
<p class="text-lg p-8">{{ $t('setup.msg') }}</p> <label for="username" class="inline-block py-2">{{
<form class="mb-8" @submit.prevent="newAccount"> $t('username')
<div> }}</label>
<label for="username" class="inline-block py-2">{{ <input
$t('username') id="username"
}}</label> v-model="username"
<input type="text"
id="username" name="username"
v-model="username" autocomplete="username"
type="text" autofocus
name="username" :placeholder="$t('setup.usernamePlaceholder')"
autocomplete="username" class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0"
:placeholder="$t('setup.usernamePlaceholder')" />
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" </div>
/> <div>
</div> <Label for="password" class="inline-block py-2">{{
<div> $t('setup.newPassword')
<Label for="password" class="inline-block py-2">{{ }}</Label>
$t('setup.newPassword') <input
}}</Label> id="password"
<input v-model="password"
id="password" type="password"
v-model="password" name="password"
type="password" autocomplete="new-password"
name="password" :placeholder="$t('setup.passwordPlaceholder')"
autocomplete="new-password" class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 focus:outline-0 focus:ring-0"
:placeholder="$t('setup.passwordPlaceholder')" />
class="px-3 py-2 text-sm dark:bg-neutral-700 text-gray-500 dark:text-gray-500 mb-5 border-2 border-gray-100 dark:border-neutral-800 rounded-lg w-full focus:border-red-800 dark:focus:border-red-800 dark:placeholder:text-neutral-400 outline-none" </div>
/> <div>
</div> <Label for="accept" class="inline-block my-4 mr-4">{{
<div> $t('setup.accept')
<Label for="accept" class="inline-block my-4 mr-4">{{ }}</Label>
$t('setup.accept') <input id="accept" v-model="accept" type="checkbox" name="accept" />
}}</Label> </div>
<input id="accept" type="checkbox" name="accept" /> <button
</div> type="submit"
<button :class="[
type="submit" {
:class="[ 'bg-red-800 dark:bg-red-800 hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer':
{ password && username,
'bg-red-800 dark:bg-red-800 hover:bg-red-700 dark:hover:bg-red-700 transition cursor-pointer': 'bg-gray-200 dark:bg-neutral-800 cursor-not-allowed':
password && username, !password && !username,
'bg-gray-200 dark:bg-neutral-800 cursor-not-allowed': },
!password && !username, 'w-max px-4 rounded shadow py-2 text-sm text-white dark:text-white',
}, ]"
'w-max px-4 rounded shadow py-2 text-sm text-white dark:text-white', >
]" {{ $t('setup.submitBtn') }}
> </button>
{{ $t('setup.submitBtn') }} </form>
</button> </PanelBody>
</form> </Panel>
</div>
<ErrorToast <ErrorToast
v-if="setupError" v-if="setupError"
:title="setupError.title" :title="setupError.title"
:message="setupError.message" :message="setupError.message"
:duration="12000"
/> />
</main> </main>
</template> </template>
@ -80,8 +78,10 @@
import { FetchError } from 'ofetch'; import { FetchError } from 'ofetch';
const { t } = useI18n(); const { t } = useI18n();
const username = ref<null | string>(null); const username = ref<null | string>(null);
const password = ref<null | string>(null); const password = ref<null | string>(null);
const accept = ref<boolean>(true);
const authStore = useAuthStore(); const authStore = useAuthStore();
type SetupError = { type SetupError = {
@ -95,7 +95,7 @@ watch(setupError, (value) => {
if (value) { if (value) {
setTimeout(() => { setTimeout(() => {
setupError.value = null; setupError.value = null;
}, 6000); }, 13000);
} }
}); });
@ -103,7 +103,11 @@ async function newAccount() {
if (!username.value || !password.value) return; if (!username.value || !password.value) return;
try { try {
const res = await authStore.signup(username.value, password.value); const res = await authStore.signup(
username.value,
password.value,
accept.value
);
if (res) { if (res) {
navigateTo('/login'); navigateTo('/login');
} }

4
src/app/stores/auth.ts

@ -9,8 +9,8 @@ export const useAuthStore = defineStore('Auth', () => {
/** /**
* @throws if unsuccessful * @throws if unsuccessful
*/ */
async function signup(username: string, password: string) { async function signup(username: string, password: string, accept: boolean) {
const response = await api.setupAccount({ username, password }); const response = await api.setupAccount({ username, password, accept });
return response.success; return response.success;
} }

4
src/app/utils/api.ts

@ -118,13 +118,15 @@ class API {
async setupAccount({ async setupAccount({
username, username,
password, password,
accept,
}: { }: {
username: string; username: string;
password: string; password: string;
accept: boolean;
}) { }) {
return $fetch('/api/account/setup', { return $fetch('/api/account/setup', {
method: 'post', method: 'post',
body: { username, password }, body: { username, password, accept },
}); });
} }
} }

4
src/locales/en.json

@ -36,6 +36,7 @@
"passwordLowercase": "Password must have at least 1 lowercase letter", "passwordLowercase": "Password must have at least 1 lowercase letter",
"passwordNumber": "Password must have at least 1 number", "passwordNumber": "Password must have at least 1 number",
"passwordSpecial": "Password must have at least 1 special character", "passwordSpecial": "Password must have at least 1 special character",
"accept": "Please accept the condition",
"remember": "Remember must be a valid boolean", "remember": "Remember must be a valid boolean",
"expireDate": "expiredDate must be a valid string", "expireDate": "expiredDate must be a valid string",
"expireDateMin": "expiredDate must be at least 1 Character", "expireDateMin": "expiredDate must be at least 1 Character",
@ -94,6 +95,7 @@
"OneTimeLink": "Generate short one time link", "OneTimeLink": "Generate short one time link",
"errorInit": "Initialization failed.", "errorInit": "Initialization failed.",
"error": { "error": {
"clear": "Clear" "clear": "Clear",
"login": "Log in error"
} }
} }

2
src/server/api/account/setup.post.ts

@ -1,7 +1,7 @@
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { username, password } = await readValidatedBody( const { username, password } = await readValidatedBody(
event, event,
validateZod(passwordType, event) validateZod(passwordSetupType, event)
); );
const users = await Database.user.findAll(); const users = await Database.user.findAll();
if (users.length !== 0) { if (users.length !== 0) {

2
src/server/api/session.post.ts

@ -3,7 +3,7 @@ import type { SessionConfig } from 'h3';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const { username, password, remember } = await readValidatedBody( const { username, password, remember } = await readValidatedBody(
event, event,
validateZod(credentialsType) validateZod(credentialsType, event)
); );
const users = await Database.user.findAll(); const users = await Database.user.findAll();

13
src/server/utils/types.ts

@ -36,6 +36,10 @@ const password = z
.regex(/[!@#$%^&*(),.?":{}|<>]/, 'zod.passwordSpecial') // i18n key .regex(/[!@#$%^&*(),.?":{}|<>]/, 'zod.passwordSpecial') // i18n key
.pipe(safeStringRefine); .pipe(safeStringRefine);
const accept = z.boolean().refine((val) => val === true, {
message: 'zod.accept',
}); // i18n key
const remember = z.boolean({ message: 'zod.remember' }); // i18n key const remember = z.boolean({ message: 'zod.remember' }); // i18n key
const expireDate = z const expireDate = z
@ -137,6 +141,15 @@ export const passwordType = z.object(
{ message: objectMessage } { message: objectMessage }
); );
export const passwordSetupType = z.object(
{
username: username,
password: password,
accept: accept,
},
{ message: objectMessage }
);
export const featuresType = z.object( export const featuresType = z.object(
{ {
features: features, features: features,

Loading…
Cancel
Save