Feat: #10 サインアップ / Chg: server-infoでconfigの取得にconfigRepo.get()を使うように / Fix: アカウントが無効でもコミュニティを作成ボタンがある問題

This commit is contained in:
2026-06-01 18:31:05 +09:00
parent 45a682b6ca
commit c725ae41bf
7 changed files with 288 additions and 21 deletions
+227
View File
@@ -0,0 +1,227 @@
<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>