0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core): register with email (#212)

This commit is contained in:
Wang Sijie 2022-02-08 20:15:36 +08:00 committed by GitHub
parent a5c9bf61d7
commit 77ca86cac6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 124 additions and 33 deletions

View file

@ -0,0 +1,99 @@
import { PasscodeType } from '@logto/schemas';
import { Context } from 'koa';
import { Provider } from 'oidc-provider';
import RequestError from '@/errors/RequestError';
import { WithUserLogContext } from '@/middleware/koa-user-log';
import { hasUser, hasUserWithEmail, insertUser } from '@/queries/user';
import assertThat from '@/utils/assert-that';
import { emailRegEx } from '@/utils/regex';
import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
import { encryptUserPassword, generateUserId } from './user';
const assignRegistrationResult = async (ctx: Context, provider: Provider, userId: string) => {
const redirectTo = await provider.interactionResult(
ctx.req,
ctx.res,
{
login: { accountId: userId },
},
{ mergeWithLastSubmission: false }
);
ctx.body = { redirectTo };
};
export const registerWithUsernameAndPassword = async (
ctx: WithUserLogContext<Context>,
provider: Provider,
username: string,
password: string
) => {
assertThat(
username && password,
new RequestError({
code: 'session.insufficient_info',
status: 400,
})
);
assertThat(
!(await hasUser(username)),
new RequestError({
code: 'user.username_exists',
status: 422,
})
);
const id = await generateUserId();
const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } =
encryptUserPassword(id, password);
await insertUser({
id,
username,
passwordEncrypted,
passwordEncryptionMethod,
passwordEncryptionSalt,
});
await assignRegistrationResult(ctx, provider, id);
};
export const sendPasscodeToEmail = async (ctx: Context, jti: string, email: string) => {
assertThat(emailRegEx.test(email), new RequestError('user.invalid_email'));
assertThat(
!(await hasUserWithEmail(email)),
new RequestError({
code: 'user.email_exists',
status: 422,
})
);
const passcode = await createPasscode(jti, PasscodeType.Register, { email });
await sendPasscode(passcode);
ctx.state = 204;
};
export const registerWithEmailAndPasscode = async (
ctx: WithUserLogContext<Context>,
provider: Provider,
{ jti, email, code }: { jti: string; email: string; code: string }
) => {
assertThat(
!(await hasUserWithEmail(email)),
new RequestError({
code: 'user.email_exists',
status: 422,
})
);
await verifyPasscode(jti, PasscodeType.Register, code, { email });
const id = await generateUserId();
await insertUser({
id,
primaryEmail: email,
});
await assignRegistrationResult(ctx, provider, id);
};

View file

@ -6,7 +6,7 @@ import RequestError from '@/errors/RequestError';
import { WithUserLogContext } from '@/middleware/koa-user-log'; import { WithUserLogContext } from '@/middleware/koa-user-log';
import { findUserByEmail, hasUserWithEmail } from '@/queries/user'; import { findUserByEmail, hasUserWithEmail } from '@/queries/user';
import assertThat from '@/utils/assert-that'; import assertThat from '@/utils/assert-that';
import { emailReg } from '@/utils/regex'; import { emailRegEx } from '@/utils/regex';
import { createPasscode, sendPasscode, verifyPasscode } from './passcode'; import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
import { findUserByUsernameAndPassword } from './user'; import { findUserByUsernameAndPassword } from './user';
@ -24,7 +24,7 @@ const assignSignInResult = async (ctx: Context, provider: Provider, userId: stri
}; };
export const sendSignInWithEmailPasscode = async (ctx: Context, jti: string, email: string) => { export const sendSignInWithEmailPasscode = async (ctx: Context, jti: string, email: string) => {
assertThat(emailReg.test(email), new RequestError('user.invalid_email')); assertThat(emailRegEx.test(email), new RequestError('user.invalid_email'));
assertThat( assertThat(
await hasUserWithEmail(email), await hasUserWithEmail(email),
new RequestError({ new RequestError({

View file

@ -4,14 +4,17 @@ import { Provider } from 'oidc-provider';
import { object, string } from 'zod'; import { object, string } from 'zod';
import RequestError from '@/errors/RequestError'; import RequestError from '@/errors/RequestError';
import {
registerWithEmailAndPasscode,
registerWithUsernameAndPassword,
sendPasscodeToEmail,
} from '@/lib/register';
import { import {
sendSignInWithEmailPasscode, sendSignInWithEmailPasscode,
signInWithEmailAndPasscode, signInWithEmailAndPasscode,
signInWithUsernameAndPassword, signInWithUsernameAndPassword,
} from '@/lib/sign-in'; } from '@/lib/sign-in';
import { encryptUserPassword, generateUserId } from '@/lib/user';
import koaGuard from '@/middleware/koa-guard'; import koaGuard from '@/middleware/koa-guard';
import { hasUser, insertUser } from '@/queries/user';
import assertThat from '@/utils/assert-that'; import assertThat from '@/utils/assert-that';
import { AnonymousRouter } from './types'; import { AnonymousRouter } from './types';
@ -108,40 +111,27 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
'/session/register', '/session/register',
koaGuard({ koaGuard({
body: object({ body: object({
username: string().min(3), username: string().min(3).optional(),
password: string().min(6), password: string().min(6).optional(),
email: string().optional(),
code: string().optional(),
}), }),
}), }),
async (ctx, next) => { async (ctx, next) => {
const { username, password } = ctx.guard.body; const interaction = await provider.interactionDetails(ctx.req, ctx.res);
const { jti } = interaction;
const { username, password, email, code } = ctx.guard.body;
if (await hasUser(username)) { if (email && !code) {
throw new RequestError('user.username_exists'); await sendPasscodeToEmail(ctx, jti, email);
} else if (email && code) {
await registerWithEmailAndPasscode(ctx, provider, { jti, email, code });
} else if (username && password) {
await registerWithUsernameAndPassword(ctx, provider, username, password);
} else {
throw new RequestError('session.insufficient_info');
} }
const id = await generateUserId();
const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } =
encryptUserPassword(id, password);
await insertUser({
id,
username,
passwordEncrypted,
passwordEncryptionMethod,
passwordEncryptionSalt,
});
const redirectTo = await provider.interactionResult(
ctx.req,
ctx.res,
{
login: { accountId: id },
},
{ mergeWithLastSubmission: false }
);
ctx.body = { redirectTo };
return next(); return next();
} }
); );

View file

@ -1 +1 @@
export const emailReg = /^\S+@\S+\.\S+$/; export const emailRegEx = /^\S+@\S+\.\S+$/;

View file

@ -45,6 +45,7 @@ const errors = {
}, },
user: { user: {
username_exists: 'The username already exists.', username_exists: 'The username already exists.',
email_exists: 'The email already exists.',
invalid_email: 'Invalid email address.', invalid_email: 'Invalid email address.',
email_not_exists: 'The email address has not been registered yet.', email_not_exists: 'The email address has not been registered yet.',
}, },

View file

@ -46,6 +46,7 @@ const errors = {
}, },
user: { user: {
username_exists: '用户名已存在。', username_exists: '用户名已存在。',
email_exists: '邮箱地址已存在。',
invalid_email: '邮箱地址不正确。', invalid_email: '邮箱地址不正确。',
email_not_exists: '邮箱地址尚未注册。', email_not_exists: '邮箱地址尚未注册。',
}, },