504 lines
12 KiB
Vue
504 lines
12 KiB
Vue
<template>
|
|
<form
|
|
class="qlvuhzng _formRoot"
|
|
autocomplete="new-password"
|
|
@submit.prevent="onSubmit"
|
|
>
|
|
<p>{{ i18n.ts.signupsDisabled }}</p>
|
|
<MkButton rounded gradate link to="https://calckey.org/join"
|
|
>{{ i18n.ts.findOtherInstance }}
|
|
</MkButton>
|
|
<MkInput
|
|
v-if="instance.disableRegistration"
|
|
v-model="invitationCode"
|
|
class="_formBlock"
|
|
type="text"
|
|
:spellcheck="false"
|
|
required
|
|
data-cy-signup-invitation-code
|
|
@update:modelValue="onChangeInvitationCode"
|
|
>
|
|
<template #label>{{ i18n.ts.invitationCode }}</template>
|
|
<template #prefix><i class="ph-key ph-bold ph-lg"></i></template>
|
|
</MkInput>
|
|
<div
|
|
v-if="
|
|
!instance.disableRegistration ||
|
|
(instance.disableRegistration && invitationState === 'entered')
|
|
"
|
|
>
|
|
<MkInput
|
|
v-model="username"
|
|
class="_formBlock"
|
|
type="text"
|
|
pattern="^[a-zA-Z0-9_]{1,20}$"
|
|
:spellcheck="false"
|
|
required
|
|
data-cy-signup-username
|
|
@update:modelValue="onChangeUsername"
|
|
>
|
|
<template #label
|
|
>{{ i18n.ts.username }}
|
|
<div
|
|
v-tooltip:dialog="i18n.ts.usernameInfo"
|
|
class="_button _help"
|
|
>
|
|
<i class="ph-question ph-bold"></i></div
|
|
></template>
|
|
<template #prefix>@</template>
|
|
<template #suffix>@{{ host }}</template>
|
|
<template #caption>
|
|
<span v-if="usernameState === 'wait'" style="color: #6e6a86"
|
|
><i
|
|
class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"
|
|
></i>
|
|
{{ i18n.ts.checking }}</span
|
|
>
|
|
<span
|
|
v-else-if="usernameState === 'ok'"
|
|
style="color: var(--success)"
|
|
><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.available }}</span
|
|
>
|
|
<span
|
|
v-else-if="usernameState === 'unavailable'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.unavailable }}</span
|
|
>
|
|
<span
|
|
v-else-if="usernameState === 'error'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.error }}</span
|
|
>
|
|
<span
|
|
v-else-if="usernameState === 'invalid-format'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.usernameInvalidFormat }}</span
|
|
>
|
|
<span
|
|
v-else-if="usernameState === 'min-range'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.tooShort }}</span
|
|
>
|
|
<span
|
|
v-else-if="usernameState === 'max-range'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.tooLong }}</span
|
|
>
|
|
</template>
|
|
</MkInput>
|
|
<MkInput
|
|
v-if="instance.emailRequiredForSignup"
|
|
v-model="email"
|
|
class="_formBlock"
|
|
:debounce="true"
|
|
type="email"
|
|
:spellcheck="false"
|
|
required
|
|
data-cy-signup-email
|
|
@update:modelValue="onChangeEmail"
|
|
>
|
|
<template #label
|
|
>{{ i18n.ts.emailAddress }}
|
|
<div
|
|
v-tooltip:dialog="i18n.ts._signup.emailAddressInfo"
|
|
class="_button _help"
|
|
>
|
|
<i class="ph-question ph-bold"></i></div
|
|
></template>
|
|
<template #prefix
|
|
><i class="ph-envelope-simple-open ph-bold ph-lg"></i
|
|
></template>
|
|
<template #caption>
|
|
<span v-if="emailState === 'wait'" style="color: #6e6a86"
|
|
><i
|
|
class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"
|
|
></i>
|
|
{{ i18n.ts.checking }}</span
|
|
>
|
|
<span
|
|
v-else-if="emailState === 'ok'"
|
|
style="color: var(--success)"
|
|
><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.available }}</span
|
|
>
|
|
<span
|
|
v-else-if="emailState === 'unavailable:used'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts._emailUnavailable.used }}</span
|
|
>
|
|
<span
|
|
v-else-if="emailState === 'unavailable:format'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts._emailUnavailable.format }}</span
|
|
>
|
|
<span
|
|
v-else-if="emailState === 'unavailable:disposable'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts._emailUnavailable.disposable }}</span
|
|
>
|
|
<span
|
|
v-else-if="emailState === 'unavailable:mx'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts._emailUnavailable.mx }}</span
|
|
>
|
|
<span
|
|
v-else-if="emailState === 'unavailable:smtp'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts._emailUnavailable.smtp }}</span
|
|
>
|
|
<span
|
|
v-else-if="emailState === 'unavailable'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.unavailable }}</span
|
|
>
|
|
<span
|
|
v-else-if="emailState === 'error'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.error }}</span
|
|
>
|
|
</template>
|
|
</MkInput>
|
|
<MkInput
|
|
v-model="password"
|
|
class="_formBlock"
|
|
type="password"
|
|
autocomplete="new-password"
|
|
required
|
|
data-cy-signup-password
|
|
@update:modelValue="onChangePassword"
|
|
>
|
|
<template #label>{{ i18n.ts.password }}</template>
|
|
<template #prefix
|
|
><i class="ph-lock ph-bold ph-lg"></i
|
|
></template>
|
|
<template #caption>
|
|
<span
|
|
v-if="passwordStrength == 'low'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.weakPassword }}</span
|
|
>
|
|
<span
|
|
v-if="passwordStrength == 'medium'"
|
|
style="color: var(--warn)"
|
|
><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.normalPassword }}</span
|
|
>
|
|
<span
|
|
v-if="passwordStrength == 'high'"
|
|
style="color: var(--success)"
|
|
><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.strongPassword }}</span
|
|
>
|
|
</template>
|
|
</MkInput>
|
|
<MkInput
|
|
v-model="retypedPassword"
|
|
class="_formBlock"
|
|
type="password"
|
|
autocomplete="new-password"
|
|
required
|
|
data-cy-signup-password-retype
|
|
@update:modelValue="onChangePasswordRetype"
|
|
>
|
|
<template #label
|
|
>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template
|
|
>
|
|
<template #prefix
|
|
><i class="ph-lock ph-bold ph-lg"></i
|
|
></template>
|
|
<template #caption>
|
|
<span
|
|
v-if="passwordRetypeState == 'match'"
|
|
style="color: var(--success)"
|
|
><i class="ph-check ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.passwordMatched }}</span
|
|
>
|
|
<span
|
|
v-if="passwordRetypeState == 'not-match'"
|
|
style="color: var(--error)"
|
|
><i class="ph-warning ph-bold ph-lg ph-fw ph-lg"></i>
|
|
{{ i18n.ts.passwordNotMatched }}</span
|
|
>
|
|
</template>
|
|
</MkInput>
|
|
<MkSwitch
|
|
v-if="instance.tosUrl"
|
|
v-model="ToSAgreement"
|
|
class="_formBlock tou"
|
|
>
|
|
<I18n :src="i18n.ts.agreeTo">
|
|
<template #0>
|
|
<a
|
|
:href="instance.tosUrl"
|
|
class="_link"
|
|
target="_blank"
|
|
>{{ i18n.ts.tos }}</a
|
|
>
|
|
</template>
|
|
</I18n>
|
|
</MkSwitch>
|
|
<MkCaptcha
|
|
v-if="instance.enableHcaptcha"
|
|
ref="hcaptcha"
|
|
v-model="hCaptchaResponse"
|
|
class="_formBlock captcha"
|
|
provider="hcaptcha"
|
|
:sitekey="instance.hcaptchaSiteKey"
|
|
/>
|
|
<MkCaptcha
|
|
v-if="instance.enableRecaptcha"
|
|
ref="recaptcha"
|
|
v-model="reCaptchaResponse"
|
|
class="_formBlock captcha"
|
|
provider="recaptcha"
|
|
:sitekey="instance.recaptchaSiteKey"
|
|
/>
|
|
<MkButton
|
|
class="_formBlock"
|
|
type="submit"
|
|
:disabled="shouldDisableSubmitting"
|
|
gradate
|
|
data-cy-signup-submit
|
|
>{{ i18n.ts.start }}</MkButton
|
|
>
|
|
</div>
|
|
</form>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import {} from "vue";
|
|
import getPasswordStrength from "syuilo-password-strength";
|
|
import { toUnicode } from "punycode/";
|
|
import MkButton from "./MkButton.vue";
|
|
import MkInput from "./form/input.vue";
|
|
import MkSwitch from "./form/switch.vue";
|
|
import MkCaptcha from "@/components/MkCaptcha.vue";
|
|
import * as config from "@/config";
|
|
import * as os from "@/os";
|
|
import { login } from "@/account";
|
|
import { instance } from "@/instance";
|
|
import { i18n } from "@/i18n";
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
autoSet?: boolean;
|
|
}>(),
|
|
{
|
|
autoSet: false,
|
|
}
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
(ev: "signup", user: Record<string, any>): void;
|
|
(ev: "signupEmailPending"): void;
|
|
}>();
|
|
|
|
const host = toUnicode(config.host);
|
|
|
|
let hcaptcha = $ref();
|
|
let recaptcha = $ref();
|
|
|
|
let username: string = $ref("");
|
|
let password: string = $ref("");
|
|
let retypedPassword: string = $ref("");
|
|
let invitationCode: string = $ref("");
|
|
let email = $ref("");
|
|
let usernameState:
|
|
| null
|
|
| "wait"
|
|
| "ok"
|
|
| "unavailable"
|
|
| "error"
|
|
| "invalid-format"
|
|
| "min-range"
|
|
| "max-range" = $ref(null);
|
|
let invitationState: null | "entered" = $ref(null);
|
|
let emailState:
|
|
| null
|
|
| "wait"
|
|
| "ok"
|
|
| "unavailable:used"
|
|
| "unavailable:format"
|
|
| "unavailable:disposable"
|
|
| "unavailable:mx"
|
|
| "unavailable:smtp"
|
|
| "unavailable"
|
|
| "error" = $ref(null);
|
|
let passwordStrength: "" | "low" | "medium" | "high" = $ref("");
|
|
let passwordRetypeState: null | "match" | "not-match" = $ref(null);
|
|
let submitting: boolean = $ref(false);
|
|
let ToSAgreement: boolean = $ref(false);
|
|
let hCaptchaResponse = $ref(null);
|
|
let reCaptchaResponse = $ref(null);
|
|
|
|
const shouldDisableSubmitting = $computed((): boolean => {
|
|
return (
|
|
submitting ||
|
|
(instance.tosUrl && !ToSAgreement) ||
|
|
(instance.enableHcaptcha && !hCaptchaResponse) ||
|
|
(instance.enableRecaptcha && !reCaptchaResponse) ||
|
|
passwordRetypeState === "not-match"
|
|
);
|
|
});
|
|
|
|
function onChangeInvitationCode(): void {
|
|
if (invitationCode === "") {
|
|
invitationState = null;
|
|
return;
|
|
}
|
|
invitationState = "entered";
|
|
}
|
|
|
|
function onChangeUsername(): void {
|
|
if (username === "") {
|
|
usernameState = null;
|
|
return;
|
|
}
|
|
|
|
{
|
|
const err = !username.match(/^[a-zA-Z0-9_]+$/)
|
|
? "invalid-format"
|
|
: username.length < 1
|
|
? "min-range"
|
|
: username.length > 20
|
|
? "max-range"
|
|
: null;
|
|
|
|
if (err) {
|
|
usernameState = err;
|
|
return;
|
|
}
|
|
}
|
|
|
|
usernameState = "wait";
|
|
|
|
os.api("username/available", {
|
|
username,
|
|
})
|
|
.then((result) => {
|
|
usernameState = result.available ? "ok" : "unavailable";
|
|
})
|
|
.catch(() => {
|
|
usernameState = "error";
|
|
});
|
|
}
|
|
|
|
function onChangeEmail(): void {
|
|
if (email === "") {
|
|
emailState = null;
|
|
return;
|
|
}
|
|
|
|
emailState = "wait";
|
|
|
|
os.api("email-address/available", {
|
|
emailAddress: email,
|
|
})
|
|
.then((result) => {
|
|
emailState = result.available
|
|
? "ok"
|
|
: result.reason === "used"
|
|
? "unavailable:used"
|
|
: result.reason === "format"
|
|
? "unavailable:format"
|
|
: result.reason === "disposable"
|
|
? "unavailable:disposable"
|
|
: result.reason === "mx"
|
|
? "unavailable:mx"
|
|
: result.reason === "smtp"
|
|
? "unavailable:smtp"
|
|
: "unavailable";
|
|
})
|
|
.catch(() => {
|
|
emailState = "error";
|
|
});
|
|
}
|
|
|
|
function onChangePassword(): void {
|
|
if (password === "") {
|
|
passwordStrength = "";
|
|
return;
|
|
}
|
|
|
|
const strength = getPasswordStrength(password);
|
|
passwordStrength =
|
|
strength > 0.7 ? "high" : strength > 0.3 ? "medium" : "low";
|
|
}
|
|
|
|
function onChangePasswordRetype(): void {
|
|
if (retypedPassword === "") {
|
|
passwordRetypeState = null;
|
|
return;
|
|
}
|
|
|
|
passwordRetypeState = password === retypedPassword ? "match" : "not-match";
|
|
}
|
|
|
|
function onSubmit(): void {
|
|
if (submitting) return;
|
|
submitting = true;
|
|
|
|
os.api("signup", {
|
|
username,
|
|
password,
|
|
emailAddress: email,
|
|
invitationCode,
|
|
"hcaptcha-response": hCaptchaResponse,
|
|
"g-recaptcha-response": reCaptchaResponse,
|
|
})
|
|
.then(() => {
|
|
if (instance.emailRequiredForSignup) {
|
|
os.alert({
|
|
type: "success",
|
|
title: i18n.ts._signup.almostThere,
|
|
text: i18n.t("_signup.emailSent", { email }),
|
|
});
|
|
emit("signupEmailPending");
|
|
} else {
|
|
os.api("signin", {
|
|
username,
|
|
password,
|
|
}).then((res) => {
|
|
emit("signup", res);
|
|
|
|
if (props.autoSet) {
|
|
login(res.i);
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.catch(() => {
|
|
submitting = false;
|
|
hcaptcha.reset?.();
|
|
recaptcha.reset?.();
|
|
|
|
os.alert({
|
|
type: "error",
|
|
text: i18n.ts.somethingHappened,
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.qlvuhzng {
|
|
.captcha {
|
|
margin: 16px 0;
|
|
}
|
|
}
|
|
</style>
|