@@ -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.5 rem ;
margin - bottom : 1.5 rem ;
}
form {
display : flex ;
flex - direction : column ;
gap : 1 rem ;
max - width : 30 rem ;
}
. input - issue {
display : flex ;
gap : 0.25 rem ;
font - size : 0.85 rem ;
color : var ( -- error - color ) ;
user - select : none ;
- webkit - user - select : none ;
}
. input - issue svg {
font - size : 1 rem ;
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 >