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:
parent
a5c9bf61d7
commit
77ca86cac6
6 changed files with 124 additions and 33 deletions
99
packages/core/src/lib/register.ts
Normal file
99
packages/core/src/lib/register.ts
Normal 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);
|
||||||
|
};
|
|
@ -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({
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export const emailReg = /^\S+@\S+\.\S+$/;
|
export const emailRegEx = /^\S+@\S+\.\S+$/;
|
||||||
|
|
|
@ -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.',
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,6 +46,7 @@ const errors = {
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
username_exists: '用户名已存在。',
|
username_exists: '用户名已存在。',
|
||||||
|
email_exists: '邮箱地址已存在。',
|
||||||
invalid_email: '邮箱地址不正确。',
|
invalid_email: '邮箱地址不正确。',
|
||||||
email_not_exists: '邮箱地址尚未注册。',
|
email_not_exists: '邮箱地址尚未注册。',
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue