mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
update cypress tests, update zod validation error messaging to UI
This commit is contained in:
parent
72ac97c5d9
commit
b1901c7278
18 changed files with 221 additions and 41 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es2020": true,
|
"es2020": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
|
"jquery": true,
|
||||||
"jest": true
|
"jest": true
|
||||||
},
|
},
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
|
|
@ -14,6 +15,7 @@
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
|
|
@ -39,7 +41,8 @@
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
"@typescript-eslint/no-empty-interface": "off",
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
|
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }],
|
||||||
|
"@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }]
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"React": "writable"
|
"React": "writable"
|
||||||
|
|
|
||||||
29
cypress/e2e/api.cy.ts
Normal file
29
cypress/e2e/api.cy.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
describe('Website tests', () => {
|
||||||
|
Cypress.session.clearAllSavedSessions();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
|
||||||
|
});
|
||||||
|
|
||||||
|
//let userId;
|
||||||
|
|
||||||
|
it('creates a user.', () => {
|
||||||
|
cy.fixture('users').then(data => {
|
||||||
|
const userPost = data.userPost;
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/users',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: Cypress.env('authorization'),
|
||||||
|
},
|
||||||
|
body: userPost,
|
||||||
|
}).then(response => {
|
||||||
|
//userId = response.body.id;
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
expect(response.body).to.have.property('username', 'cypress1');
|
||||||
|
expect(response.body).to.have.property('role', 'User');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,22 +1,36 @@
|
||||||
describe('Login tests', () => {
|
describe('Login tests', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/login');
|
||||||
|
});
|
||||||
|
|
||||||
it(
|
it(
|
||||||
'logs user in with correct credentials and logs user out',
|
'logs user in with correct credentials and logs user out',
|
||||||
{
|
{
|
||||||
defaultCommandTimeout: 10000,
|
defaultCommandTimeout: 10000,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
cy.visit('/login');
|
cy.getDataTest('input-username').find('input').as('inputUsername').click();
|
||||||
cy.getDataTest('input-username').find('input').click();
|
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
|
||||||
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 });
|
cy.get('@inputUsername').click();
|
||||||
cy.getDataTest('input-password').find('input').click();
|
|
||||||
cy.getDataTest('input-password')
|
cy.getDataTest('input-password')
|
||||||
.find('input')
|
.find('input')
|
||||||
.type(Cypress.env('umami_password'), { delay: 50 });
|
.type(Cypress.env('umami_password'), { delay: 0 });
|
||||||
cy.getDataTest('button-submit').click();
|
cy.getDataTest('button-submit').click();
|
||||||
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
||||||
cy.getDataTest('button-profile').click();
|
cy.logout();
|
||||||
cy.getDataTest('item-logout').click();
|
|
||||||
cy.url().should('eq', Cypress.config().baseUrl + '/login');
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it('login with blank inputs or incorrect credentials', () => {
|
||||||
|
cy.getDataTest('button-submit').click();
|
||||||
|
cy.contains(/Required/i).should('be.visible');
|
||||||
|
|
||||||
|
cy.getDataTest('input-username').find('input').as('inputUsername');
|
||||||
|
cy.get('@inputUsername').click();
|
||||||
|
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
|
||||||
|
cy.get('@inputUsername').click();
|
||||||
|
cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 });
|
||||||
|
cy.getDataTest('button-submit').click();
|
||||||
|
cy.contains(/Incorrect username and\/or password./i).should('be.visible');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
65
cypress/e2e/user.cy.ts
Normal file
65
cypress/e2e/user.cy.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
describe('Website tests', () => {
|
||||||
|
Cypress.session.clearAllSavedSessions();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
|
||||||
|
cy.visit('/settings/users');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add a User', () => {
|
||||||
|
// add user
|
||||||
|
cy.contains(/Create user/i).should('be.visible');
|
||||||
|
cy.getDataTest('button-create-user').click();
|
||||||
|
cy.getDataTest('input-username').find('input').as('inputName').click();
|
||||||
|
cy.get('@inputName').type('Test-user', { delay: 0 });
|
||||||
|
cy.getDataTest('input-password').find('input').as('inputPassword').click();
|
||||||
|
cy.get('@inputPassword').type('testPasswordCypress', { delay: 0 });
|
||||||
|
cy.getDataTest('dropdown-role').click();
|
||||||
|
cy.getDataTest('dropdown-item-user').click();
|
||||||
|
cy.getDataTest('button-submit').click();
|
||||||
|
cy.get('td[label="Username"]').should('contain.text', 'Test-user');
|
||||||
|
cy.get('td[label="Role"]').should('contain.text', 'User');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edit a User role and password', () => {
|
||||||
|
// edit user
|
||||||
|
cy.get('table tbody tr')
|
||||||
|
.contains('td', /Test-user/i)
|
||||||
|
.parent()
|
||||||
|
.within(() => {
|
||||||
|
cy.getDataTest('link-button-edit').click(); // Clicks the button inside the row
|
||||||
|
});
|
||||||
|
cy.getDataTest('input-password').find('input').as('inputPassword').click();
|
||||||
|
cy.get('@inputPassword').type('newPassword', { delay: 0 });
|
||||||
|
cy.getDataTest('dropdown-role').click();
|
||||||
|
cy.getDataTest('dropdown-item-viewOnly').click();
|
||||||
|
cy.getDataTest('button-submit').click();
|
||||||
|
|
||||||
|
cy.visit('/settings/users');
|
||||||
|
cy.get('table tbody tr')
|
||||||
|
.contains('td', /Test-user/i)
|
||||||
|
.parent()
|
||||||
|
.should('contain.text', 'View only');
|
||||||
|
|
||||||
|
cy.logout();
|
||||||
|
cy.url().should('eq', Cypress.config().baseUrl + '/login');
|
||||||
|
cy.getDataTest('input-username').find('input').as('inputUsername').click();
|
||||||
|
cy.get('@inputUsername').type('Test-user', { delay: 0 });
|
||||||
|
cy.get('@inputUsername').click();
|
||||||
|
cy.getDataTest('input-password').find('input').type('newPassword', { delay: 0 });
|
||||||
|
cy.getDataTest('button-submit').click();
|
||||||
|
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Delete a website', () => {
|
||||||
|
// delete user
|
||||||
|
cy.get('table tbody tr')
|
||||||
|
.contains('td', /Test-user/i)
|
||||||
|
.parent()
|
||||||
|
.within(() => {
|
||||||
|
cy.getDataTest('button-delete').click(); // Clicks the button inside the row
|
||||||
|
});
|
||||||
|
cy.contains(/Are you sure you want to delete Test-user?/i).should('be.visible');
|
||||||
|
cy.getDataTest('button-confirm').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -10,10 +10,10 @@ describe('Website tests', () => {
|
||||||
cy.visit('/settings/websites');
|
cy.visit('/settings/websites');
|
||||||
cy.getDataTest('button-website-add').click();
|
cy.getDataTest('button-website-add').click();
|
||||||
cy.contains(/Add website/i).should('be.visible');
|
cy.contains(/Add website/i).should('be.visible');
|
||||||
cy.getDataTest('input-name').find('input').click();
|
cy.getDataTest('input-name').find('input').as('inputUsername').click();
|
||||||
cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 });
|
cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 });
|
||||||
cy.getDataTest('input-domain').find('input').click();
|
cy.getDataTest('input-domain').find('input').click();
|
||||||
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 });
|
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 0 });
|
||||||
cy.getDataTest('button-submit').click();
|
cy.getDataTest('button-submit').click();
|
||||||
cy.get('td[label="Name"]').should('contain.text', 'Add test');
|
cy.get('td[label="Name"]').should('contain.text', 'Add test');
|
||||||
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
|
||||||
|
|
@ -41,10 +41,10 @@ describe('Website tests', () => {
|
||||||
cy.contains(/Details/i).should('be.visible');
|
cy.contains(/Details/i).should('be.visible');
|
||||||
cy.getDataTest('input-name').find('input').click();
|
cy.getDataTest('input-name').find('input').click();
|
||||||
cy.getDataTest('input-name').find('input').clear();
|
cy.getDataTest('input-name').find('input').clear();
|
||||||
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 });
|
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 0 });
|
||||||
cy.getDataTest('input-domain').find('input').click();
|
cy.getDataTest('input-domain').find('input').click();
|
||||||
cy.getDataTest('input-domain').find('input').clear();
|
cy.getDataTest('input-domain').find('input').clear();
|
||||||
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 });
|
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 0 });
|
||||||
cy.getDataTest('button-submit').click({ force: true });
|
cy.getDataTest('button-submit').click({ force: true });
|
||||||
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
|
||||||
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
|
||||||
|
|
|
||||||
17
cypress/fixtures/users.json
Normal file
17
cypress/fixtures/users.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"userGet": {
|
||||||
|
"name": "cypress",
|
||||||
|
"email": "password",
|
||||||
|
"role": "User"
|
||||||
|
},
|
||||||
|
"userPost": {
|
||||||
|
"username": "cypress1",
|
||||||
|
"password": "password",
|
||||||
|
"role": "User"
|
||||||
|
},
|
||||||
|
"userDelete": {
|
||||||
|
"name": "Charlie",
|
||||||
|
"email": "charlie@example.com",
|
||||||
|
"age": 35
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,12 @@ Cypress.Commands.add('getDataTest', (value: string) => {
|
||||||
return cy.get(`[data-test=${value}]`);
|
return cy.get(`[data-test=${value}]`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('logout', () => {
|
||||||
|
cy.getDataTest('button-profile').click();
|
||||||
|
cy.getDataTest('item-logout').click();
|
||||||
|
cy.url().should('eq', Cypress.config().baseUrl + '/login');
|
||||||
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('login', (username: string, password: string) => {
|
Cypress.Commands.add('login', (username: string, password: string) => {
|
||||||
cy.session([username, password], () => {
|
cy.session([username, password], () => {
|
||||||
cy.request({
|
cy.request({
|
||||||
|
|
|
||||||
6
cypress/support/index.d.ts
vendored
6
cypress/support/index.d.ts
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
/* global JQuery */
|
||||||
|
|
||||||
declare namespace Cypress {
|
declare namespace Cypress {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
|
|
@ -7,6 +8,11 @@ declare namespace Cypress {
|
||||||
* @example cy.getDataTest('greeting')
|
* @example cy.getDataTest('greeting')
|
||||||
*/
|
*/
|
||||||
getDataTest(value: string): Chainable<JQuery<HTMLElement>>;
|
getDataTest(value: string): Chainable<JQuery<HTMLElement>>;
|
||||||
|
/**
|
||||||
|
* Custom command to logout through UI.
|
||||||
|
* @example cy.logout()
|
||||||
|
*/
|
||||||
|
logout(): Chainable<JQuery<HTMLElement>>;
|
||||||
/**
|
/**
|
||||||
* Custom command to login user into the app.
|
* Custom command to login user into the app.
|
||||||
* @example cy.login('admin', 'password)
|
* @example cy.login('admin', 'password)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export function UserAddButton({ onSave }: { onSave?: () => void }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalTrigger>
|
<ModalTrigger>
|
||||||
<Button variant="primary">
|
<Button data-test="button-create-user" variant="primary">
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Plus />
|
<Icons.Plus />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import { useApi, useMessages } from '@/components/hooks';
|
import { useApi, useMessages } from '@/components/hooks';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
import { messages } from '@/components/messages';
|
||||||
|
|
||||||
export function UserAddForm({ onSave, onClose }) {
|
export function UserAddForm({ onSave, onClose }) {
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
|
|
@ -44,26 +45,43 @@ export function UserAddForm({ onSave, onClose }) {
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={error}>
|
<Form onSubmit={handleSubmit} error={error}>
|
||||||
<FormRow label={formatMessage(labels.username)}>
|
<FormRow label={formatMessage(labels.username)}>
|
||||||
<FormInput name="username" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput
|
||||||
|
data-test="input-username"
|
||||||
|
name="username"
|
||||||
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
|
>
|
||||||
<TextField autoComplete="new-username" />
|
<TextField autoComplete="new-username" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.password)}>
|
<FormRow label={formatMessage(labels.password)}>
|
||||||
<FormInput name="password" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput
|
||||||
|
data-test="input-password"
|
||||||
|
name="password"
|
||||||
|
rules={{
|
||||||
|
required: formatMessage(labels.required),
|
||||||
|
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PasswordField autoComplete="new-password" />
|
<PasswordField autoComplete="new-password" />
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.role)}>
|
<FormRow label={formatMessage(labels.role)}>
|
||||||
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
||||||
<Dropdown renderValue={renderValue}>
|
<Dropdown data-test="dropdown-role" renderValue={renderValue}>
|
||||||
<Item key={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</Item>
|
<Item data-test="dropdown-item-viewOnly" key={ROLES.viewOnly}>
|
||||||
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
|
{formatMessage(labels.viewOnly)}
|
||||||
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
|
</Item>
|
||||||
|
<Item data-test="dropdown-item-user" key={ROLES.user}>
|
||||||
|
{formatMessage(labels.user)}
|
||||||
|
</Item>
|
||||||
|
<Item data-test="dropdown-item-admin" key={ROLES.admin}>
|
||||||
|
{formatMessage(labels.admin)}
|
||||||
|
</Item>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormButtons flex>
|
<FormButtons flex>
|
||||||
<SubmitButton variant="primary" disabled={false}>
|
<SubmitButton data-test="button-submit" variant="primary" disabled={false}>
|
||||||
{formatMessage(labels.save)}
|
{formatMessage(labels.save)}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
<Button disabled={isPending} onClick={onClose}>
|
<Button disabled={isPending} onClick={onClose}>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export function UserDeleteButton({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalTrigger disabled={userId === user?.id}>
|
<ModalTrigger disabled={userId === user?.id}>
|
||||||
<Button disabled={userId === user?.id} variant="quiet">
|
<Button data-test="button-delete" disabled={userId === user?.id} variant="quiet">
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Trash />
|
<Icons.Trash />
|
||||||
</Icon>
|
</Icon>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export function UsersTable({
|
||||||
<>
|
<>
|
||||||
<UserDeleteButton userId={id} username={username} />
|
<UserDeleteButton userId={id} username={username} />
|
||||||
<LinkButton href={`/settings/users/${id}`}>
|
<LinkButton href={`/settings/users/${id}`}>
|
||||||
<Icon>
|
<Icon data-test="link-button-edit">
|
||||||
<Icons.Edit />
|
<Icons.Edit />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(labels.edit)}</Text>
|
<Text>{formatMessage(labels.edit)}</Text>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow label={formatMessage(labels.password)}>
|
<FormRow label={formatMessage(labels.password)}>
|
||||||
<FormInput
|
<FormInput
|
||||||
|
data-test="input-password"
|
||||||
name="password"
|
name="password"
|
||||||
rules={{
|
rules={{
|
||||||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||||
|
|
@ -73,16 +74,24 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
||||||
{user.id !== login.id && (
|
{user.id !== login.id && (
|
||||||
<FormRow label={formatMessage(labels.role)}>
|
<FormRow label={formatMessage(labels.role)}>
|
||||||
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
|
||||||
<Dropdown renderValue={renderValue}>
|
<Dropdown data-test="dropdown-role" renderValue={renderValue}>
|
||||||
<Item key={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</Item>
|
<Item data-test="dropdown-item-viewOnly" key={ROLES.viewOnly}>
|
||||||
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
|
{formatMessage(labels.viewOnly)}
|
||||||
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
|
</Item>
|
||||||
|
<Item data-test="dropdown-item-user" key={ROLES.user}>
|
||||||
|
{formatMessage(labels.user)}
|
||||||
|
</Item>
|
||||||
|
<Item data-test="dropdown-item-admin" key={ROLES.admin}>
|
||||||
|
{formatMessage(labels.admin)}
|
||||||
|
</Item>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
)}
|
)}
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
|
<SubmitButton data-test="button-submit" variant="primary">
|
||||||
|
{formatMessage(labels.save)}
|
||||||
|
</SubmitButton>
|
||||||
</FormButtons>
|
</FormButtons>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export async function POST(request: Request) {
|
||||||
const user = await getUserByUsername(username, { includePassword: true });
|
const user = await getUserByUsername(username, { includePassword: true });
|
||||||
|
|
||||||
if (!user || !checkPassword(password, user.password)) {
|
if (!user || !checkPassword(password, user.password)) {
|
||||||
return unauthorized();
|
return unauthorized('message.incorrect-username-password');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, role, createdAt } = user;
|
const { id, role, createdAt } = user;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { z } from 'zod';
|
import { canDeleteUser, canUpdateUser, canViewUser, hashPassword } from '@/lib/auth';
|
||||||
import { canUpdateUser, canViewUser, canDeleteUser } from '@/lib/auth';
|
|
||||||
import { getUser, getUserByUsername, updateUser, deleteUser } from '@/queries';
|
|
||||||
import { json, unauthorized, badRequest, ok } from '@/lib/response';
|
|
||||||
import { hashPassword } from '@/lib/auth';
|
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
|
import { badRequest, json, ok, unauthorized } from '@/lib/response';
|
||||||
|
import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
||||||
const { auth, error } = await parseRequest(request);
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
@ -26,7 +25,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
|
||||||
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string().max(255),
|
username: z.string().max(255),
|
||||||
password: z.string().max(255),
|
password: z.string().max(255).optional(),
|
||||||
role: z.string().regex(/admin|user|view-only/i),
|
role: z.string().regex(/admin|user|view-only/i),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import Logo from '@/assets/logo.svg';
|
||||||
import styles from './LoginForm.module.css';
|
import styles from './LoginForm.module.css';
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels, getMessage } = useMessages();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const { mutate, error, isPending } = useMutation({
|
const { mutate, error, isPending } = useMutation({
|
||||||
|
|
@ -40,7 +40,7 @@ export function LoginForm() {
|
||||||
<Logo />
|
<Logo />
|
||||||
</Icon>
|
</Icon>
|
||||||
<div className={styles.title}>umami</div>
|
<div className={styles.title}>umami</div>
|
||||||
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
|
<Form className={styles.form} onSubmit={handleSubmit} error={getMessage(error)}>
|
||||||
<FormRow label={formatMessage(labels.username)}>
|
<FormRow label={formatMessage(labels.username)}>
|
||||||
<FormInput
|
<FormInput
|
||||||
data-test="input-username"
|
data-test="input-username"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,12 @@ export function ConfirmationForm({
|
||||||
<Form error={error}>
|
<Form error={error}>
|
||||||
<p>{message}</p>
|
<p>{message}</p>
|
||||||
<FormButtons flex>
|
<FormButtons flex>
|
||||||
<LoadingButton isLoading={isLoading} onClick={onConfirm} variant={buttonVariant}>
|
<LoadingButton
|
||||||
|
data-test="button-confirm"
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={onConfirm}
|
||||||
|
variant={buttonVariant}
|
||||||
|
>
|
||||||
{buttonLabel || formatMessage(labels.ok)}
|
{buttonLabel || formatMessage(labels.ok)}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ZodSchema } from 'zod';
|
import { z, ZodSchema } from 'zod';
|
||||||
import { FILTER_COLUMNS } from '@/lib/constants';
|
import { FILTER_COLUMNS } from '@/lib/constants';
|
||||||
import { badRequest, unauthorized } from '@/lib/response';
|
import { badRequest, unauthorized } from '@/lib/response';
|
||||||
import { getAllowedUnits, getMinimumUnit } from '@/lib/date';
|
import { getAllowedUnits, getMinimumUnit } from '@/lib/date';
|
||||||
|
|
@ -24,12 +24,21 @@ export async function parseRequest(
|
||||||
let error: () => void | undefined;
|
let error: () => void | undefined;
|
||||||
let auth = null;
|
let auth = null;
|
||||||
|
|
||||||
|
const getErrorMessages = (error: z.ZodError) => {
|
||||||
|
return Object.entries(error.format())
|
||||||
|
.map(([key, value]) => {
|
||||||
|
const messages = (value as any)._errors;
|
||||||
|
return messages ? `${key}: ${messages.join(', ')}` : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
if (schema) {
|
if (schema) {
|
||||||
const isGet = request.method === 'GET';
|
const isGet = request.method === 'GET';
|
||||||
const result = schema.safeParse(isGet ? query : body);
|
const result = schema.safeParse(isGet ? query : body);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
error = () => badRequest(result.error);
|
error = () => badRequest(getErrorMessages(result.error));
|
||||||
} else if (isGet) {
|
} else if (isGet) {
|
||||||
query = result.data;
|
query = result.data;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue