mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -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,
|
findUserById,
|
||||||
hasUser,
|
hasUser,
|
||||||
updateUserById,
|
updateUserById,
|
||||||
|
hasUserWithEmail,
|
||||||
} from '@/queries/user';
|
} from '@/queries/user';
|
||||||
import assertThat from '@/utils/assert-that';
|
import assertThat from '@/utils/assert-that';
|
||||||
|
|
||||||
|
@ -121,15 +122,24 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
||||||
'/users',
|
'/users',
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: object({
|
body: object({
|
||||||
username: string().regex(usernameRegEx),
|
primaryEmail: string().regex(emailRegEx).optional(),
|
||||||
|
username: string().regex(usernameRegEx).optional(),
|
||||||
password: string().regex(passwordRegEx),
|
password: string().regex(passwordRegEx),
|
||||||
name: string(),
|
name: string(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { username, password, name } = ctx.guard.body;
|
const { primaryEmail, username, password, name } = ctx.guard.body;
|
||||||
|
|
||||||
assertThat(
|
assertThat(
|
||||||
!(await hasUser(username)),
|
!username || !(await hasUser(username)),
|
||||||
|
new RequestError({
|
||||||
|
code: 'user.username_exists_register',
|
||||||
|
status: 422,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assertThat(
|
||||||
|
!primaryEmail || !(await hasUserWithEmail(primaryEmail)),
|
||||||
new RequestError({
|
new RequestError({
|
||||||
code: 'user.username_exists_register',
|
code: 'user.username_exists_register',
|
||||||
status: 422,
|
status: 422,
|
||||||
|
@ -142,6 +152,7 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
||||||
|
|
||||||
const user = await insertUser({
|
const user = await insertUser({
|
||||||
id,
|
id,
|
||||||
|
primaryEmail,
|
||||||
username,
|
username,
|
||||||
passwordEncrypted,
|
passwordEncrypted,
|
||||||
passwordEncryptionMethod,
|
passwordEncryptionMethod,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { User } from '@logto/schemas';
|
||||||
import { authedAdminApi } from './api';
|
import { authedAdminApi } from './api';
|
||||||
|
|
||||||
type CreateUserPayload = {
|
type CreateUserPayload = {
|
||||||
|
primaryEmail?: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -24,17 +24,27 @@ export const registerUserWithUsernameAndPassword = async (
|
||||||
})
|
})
|
||||||
.json<RedirectResponse>();
|
.json<RedirectResponse>();
|
||||||
|
|
||||||
export const signInWithUsernameAndPassword = async (
|
export type SignInWithPassword = {
|
||||||
username: string,
|
username?: string;
|
||||||
password: string,
|
email?: string;
|
||||||
interactionCookie: string
|
password: string;
|
||||||
) =>
|
interactionCookie: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const signInWithPassword = async ({
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
interactionCookie,
|
||||||
|
}: SignInWithPassword) =>
|
||||||
api
|
api
|
||||||
.post('session/sign-in/password/username', {
|
// This route in core needs to be refactored
|
||||||
|
.post('session/sign-in/password/' + (username ? 'username' : 'email'), {
|
||||||
headers: {
|
headers: {
|
||||||
cookie: interactionCookie,
|
cookie: interactionCookie,
|
||||||
},
|
},
|
||||||
json: {
|
json: {
|
||||||
|
email,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
},
|
},
|
||||||
|
@ -42,6 +52,24 @@ export const signInWithUsernameAndPassword = async (
|
||||||
})
|
})
|
||||||
.json<RedirectResponse>();
|
.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) =>
|
export const consent = async (interactionCookie: string) =>
|
||||||
api
|
api
|
||||||
.post('session/consent', {
|
.post('session/consent', {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { HTTPError } from 'got';
|
||||||
import {
|
import {
|
||||||
createUser,
|
createUser,
|
||||||
registerUserWithUsernameAndPassword,
|
registerUserWithUsernameAndPassword,
|
||||||
signInWithUsernameAndPassword,
|
signInWithPassword,
|
||||||
updateConnectorConfig,
|
updateConnectorConfig,
|
||||||
enableConnector,
|
enableConnector,
|
||||||
bindWithSocial,
|
bindWithSocial,
|
||||||
|
@ -21,14 +21,12 @@ import { generateUsername, generatePassword } from '@/utils';
|
||||||
|
|
||||||
import { mockSocialConnectorId } from './__mocks__/connectors-mock';
|
import { mockSocialConnectorId } from './__mocks__/connectors-mock';
|
||||||
|
|
||||||
export const createUserByAdmin = (_username?: string, _password?: string) => {
|
export const createUserByAdmin = (username?: string, password?: string, primaryEmail?: string) => {
|
||||||
const username = _username ?? generateUsername();
|
|
||||||
const password = _password ?? generatePassword();
|
|
||||||
|
|
||||||
return createUser({
|
return createUser({
|
||||||
username,
|
username: username ?? generateUsername(),
|
||||||
password,
|
password: password ?? generatePassword(),
|
||||||
name: username,
|
name: username ?? 'John',
|
||||||
|
primaryEmail,
|
||||||
}).json<User>();
|
}).json<User>();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -49,17 +47,24 @@ export const registerNewUser = async (username: string, password: string) => {
|
||||||
assert(client.isAuthenticated, new Error('Sign in failed'));
|
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();
|
const client = new MockClient();
|
||||||
await client.initSession();
|
await client.initSession();
|
||||||
|
|
||||||
assert(client.interactionCookie, new Error('Session not found'));
|
assert(client.interactionCookie, new Error('Session not found'));
|
||||||
|
|
||||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
const { redirectTo } = await signInWithPassword({
|
||||||
username,
|
username,
|
||||||
|
email,
|
||||||
password,
|
password,
|
||||||
client.interactionCookie
|
interactionCookie: client.interactionCookie,
|
||||||
);
|
});
|
||||||
|
|
||||||
await client.processSession(redirectTo);
|
await client.processSession(redirectTo);
|
||||||
|
|
||||||
|
@ -135,11 +140,11 @@ export const bindSocialToNewCreatedUser = async () => {
|
||||||
new Error('Auth with social failed')
|
new Error('Auth with social failed')
|
||||||
);
|
);
|
||||||
|
|
||||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
const { redirectTo } = await signInWithPassword({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
client.interactionCookie
|
interactionCookie: client.interactionCookie,
|
||||||
);
|
});
|
||||||
|
|
||||||
await bindWithSocial(mockSocialConnectorId, 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 generateResourceName = () => `res_${crypto.randomUUID()}`;
|
||||||
export const generateResourceIndicator = () => `https://${crypto.randomUUID()}.logto.io`;
|
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 = () => {
|
export const generatePhone = () => {
|
||||||
const array = new Uint32Array(1);
|
const array = new Uint32Array(1);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { managementResource } from '@logto/schemas/lib/seeds';
|
||||||
import { assert } from '@silverhand/essentials';
|
import { assert } from '@silverhand/essentials';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
import { signInWithUsernameAndPassword } from '@/api';
|
import { signInWithPassword } from '@/api';
|
||||||
import MockClient, { defaultConfig } from '@/client';
|
import MockClient, { defaultConfig } from '@/client';
|
||||||
import { logtoUrl } from '@/constants';
|
import { logtoUrl } from '@/constants';
|
||||||
import { createUserByAdmin } from '@/helpers';
|
import { createUserByAdmin } from '@/helpers';
|
||||||
|
@ -24,11 +24,11 @@ describe('get access token', () => {
|
||||||
await client.initSession();
|
await client.initSession();
|
||||||
assert(client.interactionCookie, new Error('Session not found'));
|
assert(client.interactionCookie, new Error('Session not found'));
|
||||||
|
|
||||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
const { redirectTo } = await signInWithPassword({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
client.interactionCookie
|
interactionCookie: client.interactionCookie,
|
||||||
);
|
});
|
||||||
|
|
||||||
await client.processSession(redirectTo);
|
await client.processSession(redirectTo);
|
||||||
|
|
||||||
|
@ -47,11 +47,11 @@ describe('get access token', () => {
|
||||||
await client.initSession();
|
await client.initSession();
|
||||||
assert(client.interactionCookie, new Error('Session not found'));
|
assert(client.interactionCookie, new Error('Session not found'));
|
||||||
|
|
||||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
const { redirectTo } = await signInWithPassword({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
client.interactionCookie
|
interactionCookie: client.interactionCookie,
|
||||||
);
|
});
|
||||||
|
|
||||||
await client.processSession(redirectTo);
|
await client.processSession(redirectTo);
|
||||||
assert(client.isAuthenticated, new Error('Sign in get get access token failed'));
|
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 { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
|
||||||
import { assert } from '@silverhand/essentials';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
mockEmailConnectorId,
|
mockEmailConnectorId,
|
||||||
|
@ -9,26 +9,27 @@ import {
|
||||||
mockSmsConnectorConfig,
|
mockSmsConnectorConfig,
|
||||||
} from '@/__mocks__/connectors-mock';
|
} from '@/__mocks__/connectors-mock';
|
||||||
import {
|
import {
|
||||||
sendRegisterUserWithEmailPasscode,
|
createUser,
|
||||||
verifyRegisterUserWithEmailPasscode,
|
|
||||||
sendSignInUserWithEmailPasscode,
|
|
||||||
verifySignInUserWithEmailPasscode,
|
|
||||||
sendRegisterUserWithSmsPasscode,
|
|
||||||
verifyRegisterUserWithSmsPasscode,
|
|
||||||
sendSignInUserWithSmsPasscode,
|
|
||||||
verifySignInUserWithSmsPasscode,
|
|
||||||
disableConnector,
|
disableConnector,
|
||||||
signInWithUsernameAndPassword,
|
sendRegisterUserWithEmailPasscode,
|
||||||
|
sendRegisterUserWithSmsPasscode,
|
||||||
|
sendSignInUserWithEmailPasscode,
|
||||||
|
sendSignInUserWithSmsPasscode,
|
||||||
|
signInWithPassword,
|
||||||
|
verifyRegisterUserWithEmailPasscode,
|
||||||
|
verifyRegisterUserWithSmsPasscode,
|
||||||
|
verifySignInUserWithEmailPasscode,
|
||||||
|
verifySignInUserWithSmsPasscode,
|
||||||
} from '@/api';
|
} from '@/api';
|
||||||
import MockClient from '@/client';
|
import MockClient from '@/client';
|
||||||
import {
|
import {
|
||||||
registerNewUser,
|
|
||||||
signIn,
|
|
||||||
setUpConnector,
|
|
||||||
readPasscode,
|
|
||||||
createUserByAdmin,
|
createUserByAdmin,
|
||||||
setSignUpIdentifier,
|
readPasscode,
|
||||||
|
registerNewUser,
|
||||||
setSignInMethod,
|
setSignInMethod,
|
||||||
|
setSignUpIdentifier,
|
||||||
|
setUpConnector,
|
||||||
|
signIn,
|
||||||
} from '@/helpers';
|
} from '@/helpers';
|
||||||
import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils';
|
import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils';
|
||||||
|
|
||||||
|
@ -36,12 +37,43 @@ describe('username and password flow', () => {
|
||||||
const username = generateUsername();
|
const username = generateUsername();
|
||||||
const password = generatePassword();
|
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(registerNewUser(username, password)).resolves.not.toThrow();
|
||||||
|
await expect(signIn({ username, password })).resolves.not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sign-in with username & password', async () => {
|
describe('email and password flow', () => {
|
||||||
await expect(signIn(username, password)).resolves.not.toThrow();
|
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('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'));
|
assert(client.interactionCookie, new Error('Session not found'));
|
||||||
|
|
||||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
const { redirectTo } = await signInWithPassword({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
client.interactionCookie
|
interactionCookie: client.interactionCookie,
|
||||||
);
|
});
|
||||||
|
|
||||||
await client.processSession(redirectTo);
|
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'));
|
assert(client.interactionCookie, new Error('Session not found'));
|
||||||
|
|
||||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
const { redirectTo } = await signInWithPassword({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
client.interactionCookie
|
interactionCookie: client.interactionCookie,
|
||||||
);
|
});
|
||||||
|
|
||||||
await client.processSession(redirectTo);
|
await client.processSession(redirectTo);
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
getAuthWithSocial,
|
getAuthWithSocial,
|
||||||
registerWithSocial,
|
registerWithSocial,
|
||||||
bindWithSocial,
|
bindWithSocial,
|
||||||
signInWithUsernameAndPassword,
|
signInWithPassword,
|
||||||
getUser,
|
getUser,
|
||||||
} from '@/api';
|
} from '@/api';
|
||||||
import MockClient from '@/client';
|
import MockClient from '@/client';
|
||||||
|
@ -126,11 +126,11 @@ describe('social bind account', () => {
|
||||||
// User with social does not exist
|
// User with social does not exist
|
||||||
expect(response instanceof HTTPError && response.response.statusCode === 422).toBe(true);
|
expect(response instanceof HTTPError && response.response.statusCode === 422).toBe(true);
|
||||||
|
|
||||||
const { redirectTo } = await signInWithUsernameAndPassword(
|
const { redirectTo } = await signInWithPassword({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
client.interactionCookie
|
interactionCookie: client.interactionCookie,
|
||||||
);
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
bindWithSocial(mockSocialConnectorId, client.interactionCookie)
|
bindWithSocial(mockSocialConnectorId, client.interactionCookie)
|
||||||
|
|
Loading…
Reference in a new issue