227 lines
6.3 KiB
Vue
227 lines
6.3 KiB
Vue
<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> |