Files
lynq-chat/packages/frontend/src/routes/signup.vue
T

227 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="welcome">
<p><!--
-->ここではサインアップを行います<!--
-->アカウントは端末に1つのものではなくIDとパスワードでどんな端末でも使いまわすことが出来ます
</p>
</div>
<form novalidate @submit="submit">
<Input
label="ユーザーID"
autocomplete="username"
v-model="userid"
>
<span
class="input-issue"
v-if="useridIssue"
>
<Icon icon="material-symbols:error-outline-rounded" />
{{ useridIssue.message }}
</span>
</Input>
<Input
label="メールアドレス"
type="email"
autocomplete="email"
v-model="email"
>
<span
class="input-issue"
v-if="emailIssue"
>
<Icon icon="material-symbols:error-outline-rounded" />
{{ emailIssue.message }}
</span>
</Input>
<InputPassword
label="パスワード"
v-model="password"
>
<span
class="input-issue"
v-if="passwordIssue"
>
<Icon icon="material-symbols:error-outline-rounded" />
{{ passwordIssue.message }}
</span>
<span
class="input-issue password-strength"
v-if="passwordStrength.message"
>
<Icon :icon="passwordStrength.icon" />
{{ passwordStrength.message }}
</span>
</InputPassword>
<Button
name="サインアップ"
type="submit"
color="accent"
:disabled="!result.success || isProcessing"
/>
<RouterLink to="/signin">アカウントをお持ちですか</RouterLink>
</form>
</template>
<style scoped>
.welcome {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 30rem;
}
.input-issue {
display: flex;
gap: 0.25rem;
font-size: 0.85rem;
color: var(--error-color);
user-select: none;
-webkit-user-select: none;
}
.input-issue svg {
font-size: 1rem;
margin: auto 0;
}
.password-strength {
color: v-bind(passwordStrengthColor);
}
</style>
<script lang="ts" setup>
import { serverInfo, reloadServerInfo } from "@/lib/account";
import { useRouter } from "vue-router";
import Input from "@/components/Input.vue";
import Button from "@/components/Button.vue";
import { ref } from "vue";
import z from "zod/v3";
import { computed } from "vue";
import zxcvbn from "zxcvbn";
import { createModal } from "@/lib/modal";
import Loading from "@/components/Modal/Loading.vue";
import ErrorModal from "@/components/Modal/Error.vue";
import Success from "@/components/Modal/Success.vue";
import InputPassword from "@/components/InputPassword.vue";
import client from "@/lib/client";
import { getIssueFromPath } from "@/lib/validation";
import { Icon } from "@iconify/vue";
const router = useRouter();
const isProcessing = ref<boolean>(false);
if (!serverInfo.value?.success) {
throw new Error("サーバー情報の取得に失敗しました。");
}
if (!serverInfo.value.isInitialized) {
router.replace("/setup/initialization");
}
const userid = ref<string>("");
const email = ref<string>("");
const password = ref<string>("");
const useridIssue = computed(() => getIssueFromPath("userid", result.value));
const emailIssue = computed(() => getIssueFromPath("email", result.value));
const passwordIssue = computed(() => getIssueFromPath("password", result.value));
const passwordScores = [
["危険なパスワード", "error-outline-rounded", "var(--error-color)"],
["推測可能なパスワード", "warning-rounded", "var(--warn-color)"],
["推測しやすいパスワード", "warning-rounded", "var(--warn-color)"],
["安全なパスワード", "check-circle", "var(--success-color)"],
["強力なパスワード", "check-circle", "var(--success-color)"],
];
const passwordStrength = computed(() => {
const evaluation = zxcvbn(password.value);
return {
message: passwordScores[evaluation.score]![0],
score: evaluation.score,
icon: "material-symbols:" + passwordScores[evaluation.score]![1],
color: passwordScores[evaluation.score]![2],
}
});
const passwordStrengthColor = computed(() => passwordStrength.value.color);
const EmailRegex = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:([0-9]{1,3}\.){3}[0-9]{1,3})\])$/i;
const schema = z.object({
userid: z.string({ message: "文字列で入力してください。" })
.trim().min(3, "3文字以上で入力してください。")
.max(20, "20文字以内で入力してください。"),
email: z.string({ message: "文字列で入力してください。" })
.trim().min(6, "6文字以上で入力してください。")
.max(254, "254文字以内で入力してください。")
.regex(EmailRegex, "形式が異なります。"),
password: z.string({ message: "文字列で入力してください。" })
.trim().min(8, "8文字以上で入力してください。"),
});
const result = computed(() => schema.safeParse({
userid: userid.value,
email: email.value,
password: password.value,
}));
const submit = async (e: Event) => {
e.preventDefault();
isProcessing.value = true;
const closeLoadingModal = createModal({
component: Loading,
});
if (!result.value.success) {
const messages = result.value.error.issues.map(issue => issue.message);
closeLoadingModal();
return createModal({
component: ErrorModal,
onClose: () => isProcessing.value = false,
props: {
error: `不正な入力です。<br>${messages.join("<br>")}`,
canClose: true,
},
});
}
const response = await client.value.request("setup/create-admin", result.value.data);
if (!response.success) {
closeLoadingModal();
return createModal({
component: ErrorModal,
onClose: () => isProcessing.value = false,
props: {
error: response.error.message,
canClose: true,
},
});
}
closeLoadingModal();
return createModal({
component: Success,
onClose: async () => {
isProcessing.value = false;
await reloadServerInfo();
router.push("/signin");
}
});
}
</script>