feat: improve email validation

This commit is contained in:
syuilo 2021-11-07 20:16:01 +09:00
parent a28c515ef6
commit 68192126e6
9 changed files with 188 additions and 26 deletions

View file

@ -19,12 +19,17 @@
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
</template>
</MkInput>
<MkInput v-if="meta.emailRequiredForSignup" class="_formBlock" v-model="email" type="email" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeEmail" data-cy-signup-email>
<MkInput v-if="meta.emailRequiredForSignup" class="_formBlock" v-model="email" :debounce="true" type="email" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeEmail" data-cy-signup-email>
<template #label>{{ $ts.emailAddress }} <div class="_button _help" v-tooltip:dialog="$ts._signup.emailAddressInfo"><i class="far fa-question-circle"></i></div></template>
<template #prefix><i class="fas fa-envelope"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
</template>
@ -171,7 +176,13 @@ export default defineComponent({
os.api('email-address/available', {
emailAddress: this.email
}).then(result => {
this.emailState = result.available ? 'ok' : 'unavailable';
this.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(err => {
this.emailState = 'error';
});

View file

@ -1,6 +1,6 @@
import $ from 'cafy';
import define from '../../define';
import { UserProfiles } from '@/models/index';
import { validateEmailForAccount } from '@/services/validate-email-for-account';
export const meta = {
tags: ['users'],
@ -20,18 +20,15 @@ export const meta = {
available: {
type: 'boolean' as const,
optional: false as const, nullable: false as const,
}
},
reason: {
type: 'string' as const,
optional: false as const, nullable: true as const,
},
}
}
};
export default define(meta, async (ps) => {
const exist = await UserProfiles.count({
emailVerified: true,
email: ps.emailAddress,
});
return {
available: exist === 0
};
return await validateEmailForAccount(ps.emailAddress);
});

View file

@ -8,6 +8,7 @@ import * as bcrypt from 'bcryptjs';
import { Users, UserProfiles } from '@/models/index';
import { sendEmail } from '@/services/send-email';
import { ApiError } from '../../error';
import { validateEmailForAccount } from '@/services/validate-email-for-account';
export const meta = {
requireCredential: true as const,
@ -35,6 +36,12 @@ export const meta = {
code: 'INCORRECT_PASSWORD',
id: 'e54c1d7e-e7d6-4103-86b6-0a95069b4ad3'
},
unavailable: {
message: 'Unavailable email address.',
code: 'UNAVAILABLE',
id: 'a2defefb-f220-8849-0af6-17f816099323'
},
}
};
@ -48,6 +55,13 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.incorrectPassword);
}
if (ps.email != null) {
const available = await validateEmailForAccount(ps.email);
if (!available) {
throw new ApiError(meta.errors.unavailable);
}
}
await UserProfiles.update(user.id, {
email: ps.email,
emailVerified: false,

View file

@ -8,6 +8,7 @@ import { signup } from '../common/signup';
import config from '@/config';
import { sendEmail } from '@/services/send-email';
import { genId } from '@/misc/gen-id';
import { validateEmailForAccount } from '@/services/validate-email-for-account';
export default async (ctx: Koa.Context) => {
const body = ctx.request.body;
@ -41,6 +42,12 @@ export default async (ctx: Koa.Context) => {
ctx.status = 400;
return;
}
const available = await validateEmailForAccount(emailAddress);
if (!available) {
ctx.status = 400;
return;
}
}
if (instance.disableRegistration) {

View file

@ -0,0 +1,34 @@
import validateEmail from 'deep-email-validator';
import { UserProfiles } from '@/models';
export async function validateEmailForAccount(emailAddress: string): Promise<{
available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
}> {
const exist = await UserProfiles.count({
emailVerified: true,
email: emailAddress,
});
const validated = await validateEmail({
email: emailAddress,
validateRegex: true,
validateMx: true,
validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
validateDisposable: true, // 捨てアドかどうかチェック
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
});
const available = exist === 0 && validated.valid;
return {
available,
reason: available ? null :
exist !== 0 ? 'used' :
validated.reason === 'regex' ? 'format' :
validated.reason === 'disposable' ? 'disposable' :
validated.reason === 'mx' ? 'mx' :
validated.reason === 'smtp' ? 'smtp' :
null,
};
}