mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(core,test): allow create user with email and add tests
- Allow create user with email or username for admin user route - Add integration tests for email and password sign-in
This commit is contained in:
parent
f82d2a8c4f
commit
4b6d3c584a
8 changed files with 138 additions and 61 deletions
|
@ -18,6 +18,7 @@ import {
|
|||
findUserById,
|
||||
hasUser,
|
||||
updateUserById,
|
||||
hasUserWithEmail,
|
||||
} from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
|
@ -121,15 +122,24 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
|||
'/users',
|
||||
koaGuard({
|
||||
body: object({
|
||||
username: string().regex(usernameRegEx),
|
||||
primaryEmail: string().regex(emailRegEx).optional(),
|
||||
username: string().regex(usernameRegEx).optional(),
|
||||
password: string().regex(passwordRegEx),
|
||||
name: string(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { username, password, name } = ctx.guard.body;
|
||||
const { primaryEmail, username, password, name } = ctx.guard.body;
|
||||
|
||||
assertThat(
|
||||
!(await hasUser(username)),
|
||||
!username || !(await hasUser(username)),
|
||||
new RequestError({
|
||||
code: 'user.username_exists_register',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
assertThat(
|
||||
!primaryEmail || !(await hasUserWithEmail(primaryEmail)),
|
||||
new RequestError({
|
||||
code: 'user.username_exists_register',
|
||||
status: 422,
|
||||
|
@ -142,6 +152,7 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
|||
|
||||
const user = await insertUser({
|
||||
id,
|
||||
primaryEmail,
|
||||
username,
|
||||
passwordEncrypted,
|
||||
passwordEncryptionMethod,
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { User } from '@logto/schemas';
|
|||
import { authedAdminApi } from './api';
|
||||
|
||||
type CreateUserPayload = {
|
||||
primaryEmail?: string;
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
|
|
|
@ -24,17 +24,27 @@ export const registerUserWithUsernameAndPassword = async (
|
|||
})
|
||||
.json<RedirectResponse>();
|
||||
|
||||
export const signInWithUsernameAndPassword = async (
|
||||
username: string,
|
||||
password: string,
|
||||
interactionCookie: string
|
||||
) =>
|
||||
export type SignInWithPassword = {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password: string;
|
||||
interactionCookie: string;
|
||||
};
|
||||
|
||||
export const signInWithPassword = async ({
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
interactionCookie,
|
||||
}: SignInWithPassword) =>
|
||||
api
|
||||
.post('session/sign-in/password/username', {
|
||||
// This route in core needs to be refactored
|
||||
.post('session/sign-in/password/' + (username ? 'username' : 'email'), {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
json: {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
|
@ -42,6 +52,24 @@ export const signInWithUsernameAndPassword = async (
|
|||
})
|
||||
.json<RedirectResponse>();
|
||||
|
||||
export const signInWithEmailAndPassword = async (
|
||||
email: string,
|
||||
password: string,
|
||||
interactionCookie: string
|
||||
) =>
|
||||
api
|
||||
.post('session/sign-in/email/username', {
|
||||
headers: {
|
||||
cookie: interactionCookie,
|
||||
},
|
||||
json: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
followRedirect: false,
|
||||
})
|
||||
.json<RedirectResponse>();
|
||||
|
||||
export const consent = async (interactionCookie: string) =>
|
||||
api
|
||||
.post('session/consent', {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { HTTPError } from 'got';
|
|||
import {
|
||||
createUser,
|
||||
registerUserWithUsernameAndPassword,
|
||||
signInWithUsernameAndPassword,
|
||||
signInWithPassword,
|
||||
updateConnectorConfig,
|
||||
enableConnector,
|
||||
bindWithSocial,
|
||||
|
@ -21,14 +21,12 @@ import { generateUsername, generatePassword } from '@/utils';
|
|||
|
||||
import { mockSocialConnectorId } from './__mocks__/connectors-mock';
|
||||
|
||||
export const createUserByAdmin = (_username?: string, _password?: string) => {
|
||||
const username = _username ?? generateUsername();
|
||||
const password = _password ?? generatePassword();
|
||||
|
||||
export const createUserByAdmin = (username?: string, password?: string, primaryEmail?: string) => {
|
||||
return createUser({
|
||||
username,
|
||||
password,
|
||||
name: username,
|
||||
username: username ?? generateUsername(),
|
||||
password: password ?? generatePassword(),
|
||||
name: username ?? 'John',
|
||||
primaryEmail,
|
||||
}).json<User>();
|
||||
};
|
||||
|
||||
|
@ -49,17 +47,24 @@ export const registerNewUser = async (username: string, password: string) => {
|
|||
assert(client.isAuthenticated, new Error('Sign in failed'));
|
||||
};
|
||||
|
||||
export const signIn = async (username: string, password: string) => {
|
||||
export type SignInHelper = {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export const signIn = async ({ username, email, password }: SignInHelper) => {
|
||||
const client = new MockClient();
|
||||
await client.initSession();
|
||||
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
const { redirectTo } = await signInWithPassword({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
interactionCookie: client.interactionCookie,
|
||||
});
|
||||
|
||||
await client.processSession(redirectTo);
|
||||
|
||||
|
@ -135,11 +140,11 @@ export const bindSocialToNewCreatedUser = async () => {
|
|||
new Error('Auth with social failed')
|
||||
);
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
const { redirectTo } = await signInWithPassword({
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
interactionCookie: client.interactionCookie,
|
||||
});
|
||||
|
||||
await bindWithSocial(mockSocialConnectorId, client.interactionCookie);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ export const generatePassword = () => `pwd_${crypto.randomUUID()}`;
|
|||
|
||||
export const generateResourceName = () => `res_${crypto.randomUUID()}`;
|
||||
export const generateResourceIndicator = () => `https://${crypto.randomUUID()}.logto.io`;
|
||||
export const generateEmail = () => `${crypto.randomUUID()}@logto.io`;
|
||||
export const generateEmail = () => `${crypto.randomUUID().toLowerCase()}@logto.io`;
|
||||
|
||||
export const generatePhone = () => {
|
||||
const array = new Uint32Array(1);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { managementResource } from '@logto/schemas/lib/seeds';
|
|||
import { assert } from '@silverhand/essentials';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { signInWithUsernameAndPassword } from '@/api';
|
||||
import { signInWithPassword } from '@/api';
|
||||
import MockClient, { defaultConfig } from '@/client';
|
||||
import { logtoUrl } from '@/constants';
|
||||
import { createUserByAdmin } from '@/helpers';
|
||||
|
@ -24,11 +24,11 @@ describe('get access token', () => {
|
|||
await client.initSession();
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
const { redirectTo } = await signInWithPassword({
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
interactionCookie: client.interactionCookie,
|
||||
});
|
||||
|
||||
await client.processSession(redirectTo);
|
||||
|
||||
|
@ -47,11 +47,11 @@ describe('get access token', () => {
|
|||
await client.initSession();
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
const { redirectTo } = await signInWithPassword({
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
interactionCookie: client.interactionCookie,
|
||||
});
|
||||
|
||||
await client.processSession(redirectTo);
|
||||
assert(client.isAuthenticated, new Error('Sign in get get access token failed'));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
|
||||
import assert from 'assert';
|
||||
|
||||
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
|
||||
import {
|
||||
mockEmailConnectorId,
|
||||
|
@ -9,26 +9,27 @@ import {
|
|||
mockSmsConnectorConfig,
|
||||
} from '@/__mocks__/connectors-mock';
|
||||
import {
|
||||
sendRegisterUserWithEmailPasscode,
|
||||
verifyRegisterUserWithEmailPasscode,
|
||||
sendSignInUserWithEmailPasscode,
|
||||
verifySignInUserWithEmailPasscode,
|
||||
sendRegisterUserWithSmsPasscode,
|
||||
verifyRegisterUserWithSmsPasscode,
|
||||
sendSignInUserWithSmsPasscode,
|
||||
verifySignInUserWithSmsPasscode,
|
||||
createUser,
|
||||
disableConnector,
|
||||
signInWithUsernameAndPassword,
|
||||
sendRegisterUserWithEmailPasscode,
|
||||
sendRegisterUserWithSmsPasscode,
|
||||
sendSignInUserWithEmailPasscode,
|
||||
sendSignInUserWithSmsPasscode,
|
||||
signInWithPassword,
|
||||
verifyRegisterUserWithEmailPasscode,
|
||||
verifyRegisterUserWithSmsPasscode,
|
||||
verifySignInUserWithEmailPasscode,
|
||||
verifySignInUserWithSmsPasscode,
|
||||
} from '@/api';
|
||||
import MockClient from '@/client';
|
||||
import {
|
||||
registerNewUser,
|
||||
signIn,
|
||||
setUpConnector,
|
||||
readPasscode,
|
||||
createUserByAdmin,
|
||||
setSignUpIdentifier,
|
||||
readPasscode,
|
||||
registerNewUser,
|
||||
setSignInMethod,
|
||||
setSignUpIdentifier,
|
||||
setUpConnector,
|
||||
signIn,
|
||||
} from '@/helpers';
|
||||
import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils';
|
||||
|
||||
|
@ -36,12 +37,43 @@ describe('username and password flow', () => {
|
|||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
|
||||
it('register with username & password', async () => {
|
||||
it('register and sign in with username & password', async () => {
|
||||
await expect(registerNewUser(username, password)).resolves.not.toThrow();
|
||||
await expect(signIn({ username, password })).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('email and password flow', () => {
|
||||
const email = generateEmail();
|
||||
const [localPart, domain] = email.split('@');
|
||||
const password = generatePassword();
|
||||
|
||||
assert(localPart && domain);
|
||||
|
||||
beforeAll(async () => {
|
||||
await setSignInMethod([
|
||||
{
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: true,
|
||||
verificationCode: false,
|
||||
isPasswordPrimary: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('sign-in with username & password', async () => {
|
||||
await expect(signIn(username, password)).resolves.not.toThrow();
|
||||
it('can sign in with email & password', async () => {
|
||||
await createUser({ password, primaryEmail: email, username: generateUsername(), name: 'John' });
|
||||
await expect(
|
||||
Promise.all([
|
||||
signIn({ email, password }),
|
||||
signIn({ email: localPart.toUpperCase() + '@' + domain, password }),
|
||||
signIn({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
email: localPart[0]! + localPart.toUpperCase().slice(1) + '@' + domain,
|
||||
password,
|
||||
}),
|
||||
])
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -236,11 +268,11 @@ describe('sign-in and sign-out', () => {
|
|||
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
const { redirectTo } = await signInWithPassword({
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
interactionCookie: client.interactionCookie,
|
||||
});
|
||||
|
||||
await client.processSession(redirectTo);
|
||||
|
||||
|
@ -266,11 +298,11 @@ describe('sign-in to demo app and revisit Admin Console', () => {
|
|||
|
||||
assert(client.interactionCookie, new Error('Session not found'));
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
const { redirectTo } = await signInWithPassword({
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
interactionCookie: client.interactionCookie,
|
||||
});
|
||||
|
||||
await client.processSession(redirectTo);
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
getAuthWithSocial,
|
||||
registerWithSocial,
|
||||
bindWithSocial,
|
||||
signInWithUsernameAndPassword,
|
||||
signInWithPassword,
|
||||
getUser,
|
||||
} from '@/api';
|
||||
import MockClient from '@/client';
|
||||
|
@ -126,11 +126,11 @@ describe('social bind account', () => {
|
|||
// User with social does not exist
|
||||
expect(response instanceof HTTPError && response.response.statusCode === 422).toBe(true);
|
||||
|
||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
||||
const { redirectTo } = await signInWithPassword({
|
||||
username,
|
||||
password,
|
||||
client.interactionCookie
|
||||
);
|
||||
interactionCookie: client.interactionCookie,
|
||||
});
|
||||
|
||||
await expect(
|
||||
bindWithSocial(mockSocialConnectorId, client.interactionCookie)
|
||||
|
|
Loading…
Reference in a new issue