mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Merge branch 'dev' into jajaja
This commit is contained in:
commit
b331da193f
27 changed files with 217 additions and 50 deletions
|
|
@ -3,6 +3,7 @@
|
|||
"browser": true,
|
||||
"es2020": true,
|
||||
"node": true,
|
||||
"jquery": true,
|
||||
"jest": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
"sourceType": "module"
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"eslint:recommended",
|
||||
"plugin:prettier/recommended",
|
||||
|
|
@ -39,7 +41,8 @@
|
|||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "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": {
|
||||
"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', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/login');
|
||||
});
|
||||
|
||||
it(
|
||||
'logs user in with correct credentials and logs user out',
|
||||
{
|
||||
defaultCommandTimeout: 10000,
|
||||
},
|
||||
() => {
|
||||
cy.visit('/login');
|
||||
cy.getDataTest('input-username').find('input').click();
|
||||
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 });
|
||||
cy.getDataTest('input-password').find('input').click();
|
||||
cy.getDataTest('input-username').find('input').as('inputUsername').click();
|
||||
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
|
||||
cy.get('@inputUsername').click();
|
||||
cy.getDataTest('input-password')
|
||||
.find('input')
|
||||
.type(Cypress.env('umami_password'), { delay: 50 });
|
||||
.type(Cypress.env('umami_password'), { delay: 0 });
|
||||
cy.getDataTest('button-submit').click();
|
||||
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
|
||||
cy.getDataTest('button-profile').click();
|
||||
cy.getDataTest('item-logout').click();
|
||||
cy.url().should('eq', Cypress.config().baseUrl + '/login');
|
||||
cy.logout();
|
||||
},
|
||||
);
|
||||
|
||||
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.getDataTest('button-website-add').click();
|
||||
cy.contains(/Add website/i).should('be.visible');
|
||||
cy.getDataTest('input-name').find('input').click();
|
||||
cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 });
|
||||
cy.getDataTest('input-name').find('input').as('inputUsername').click();
|
||||
cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 });
|
||||
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.get('td[label="Name"]').should('contain.text', 'Add test');
|
||||
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.getDataTest('input-name').find('input').click();
|
||||
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').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('input-name').find('input').should('have.value', 'Updated website');
|
||||
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}]`);
|
||||
});
|
||||
|
||||
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) => {
|
||||
cy.session([username, password], () => {
|
||||
cy.request({
|
||||
|
|
|
|||
6
cypress/support/index.d.ts
vendored
6
cypress/support/index.d.ts
vendored
|
|
@ -1,4 +1,5 @@
|
|||
/// <reference types="cypress" />
|
||||
/* global JQuery */
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
|
@ -7,6 +8,11 @@ declare namespace Cypress {
|
|||
* @example cy.getDataTest('greeting')
|
||||
*/
|
||||
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.
|
||||
* @example cy.login('admin', 'password)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "umami",
|
||||
"version": "2.16.1",
|
||||
"version": "2.17.0",
|
||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||
"license": "MIT",
|
||||
|
|
@ -120,6 +120,7 @@
|
|||
"react-simple-maps": "^2.3.0",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"react-window": "^1.8.11",
|
||||
"react-zen": "link:C:/Users/mike/AppData/Local/pnpm/global/5/node_modules/@umami/react-zen",
|
||||
"request-ip": "^3.3.0",
|
||||
"semver": "^7.7.1",
|
||||
"serialize-error": "^12.0.0",
|
||||
|
|
@ -196,7 +197,7 @@
|
|||
"sharp"
|
||||
],
|
||||
"overrides": {
|
||||
"@umami/react-zen": "link:C:/Users/mike/AppData/Local/pnpm/global/5/node_modules/@umami/react-zen"
|
||||
"react-zen": "link:C:/Users/mike/AppData/Local/pnpm/global/5/node_modules/@umami/react-zen"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
pnpm-lock.yaml
generated
5
pnpm-lock.yaml
generated
|
|
@ -5,7 +5,7 @@ settings:
|
|||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
'@umami/react-zen': link:C:/Users/mike/AppData/Local/pnpm/global/5/node_modules/@umami/react-zen
|
||||
react-zen: link:C:/Users/mike/AppData/Local/pnpm/global/5/node_modules/@umami/react-zen
|
||||
|
||||
importers:
|
||||
|
||||
|
|
@ -176,6 +176,9 @@ importers:
|
|||
react-window:
|
||||
specifier: ^1.8.11
|
||||
version: 1.8.11(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react-zen:
|
||||
specifier: link:C:/Users/mike/AppData/Local/pnpm/global/5/node_modules/@umami/react-zen
|
||||
version: link:C:/Users/mike/AppData/Local/pnpm/global/5/node_modules/@umami/react-zen
|
||||
request-ip:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export function ReportDeleteButton({
|
|||
{({ close }) => (
|
||||
<ConfirmationForm
|
||||
message={formatMessage(messages.confirmDelete, {
|
||||
target: <b key="report-name">{reportName}</b>,
|
||||
target: <b key={messages.confirmDelete.id}>{reportName}</b>,
|
||||
})}
|
||||
isLoading={isPending}
|
||||
error={error}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ export function TeamLeaveForm({
|
|||
return (
|
||||
<ConfirmationForm
|
||||
buttonLabel={formatMessage(labels.leave)}
|
||||
message={formatMessage(messages.confirmLeave, { target: <b>{teamName}</b> })}
|
||||
message={formatMessage(messages.confirmLeave, {
|
||||
target: <b key={messages.confirmLeave.id}>{teamName}</b>,
|
||||
})}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={onClose}
|
||||
isLoading={isPending}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function UserAddButton({ onSave }: { onSave?: () => void }) {
|
|||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="primary">
|
||||
<Button variant="primary" data-test="button-create-user">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '@umami/react-zen';
|
||||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
import { messages } from '@/components/messages';
|
||||
|
||||
export function UserAddForm({ onSave, onClose }) {
|
||||
const { post, useMutation } = useApi();
|
||||
|
|
@ -35,14 +36,14 @@ export function UserAddForm({ onSave, onClose }) {
|
|||
name="username"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<TextField autoComplete="new-username" />
|
||||
<TextField autoComplete="new-username" data-test="input-username" />
|
||||
</FormField>
|
||||
<FormField
|
||||
label={formatMessage(labels.password)}
|
||||
name="password"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<PasswordField autoComplete="new-password" />
|
||||
<PasswordField autoComplete="new-password" data-test="input-password" />
|
||||
</FormField>
|
||||
<FormField
|
||||
label={formatMessage(labels.role)}
|
||||
|
|
@ -50,13 +51,13 @@ export function UserAddForm({ onSave, onClose }) {
|
|||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<Select>
|
||||
<ListItem id={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</ListItem>
|
||||
<ListItem id={ROLES.user}>{formatMessage(labels.user)}</ListItem>
|
||||
<ListItem id={ROLES.admin}>{formatMessage(labels.admin)}</ListItem>
|
||||
<ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">{formatMessage(labels.viewOnly)}</ListItem>
|
||||
<ListItem id={ROLES.user} data-test="dropdown-item-user">{formatMessage(labels.user)}</ListItem>
|
||||
<ListItem id={ROLES.admin} data-test="dropdown-item-admin">{formatMessage(labels.admin)}</ListItem>
|
||||
</Select>
|
||||
</FormField>
|
||||
<FormButtons>
|
||||
<FormSubmitButton variant="primary" disabled={false}>
|
||||
<FormSubmitButton variant="primary" data-test="button-submit" isDisabled={false}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export function UserDeleteButton({
|
|||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button isDisabled={userId === user?.id}>
|
||||
<Button isDisabled={userId === user?.id} data-test="button-delete">
|
||||
<Icon size="sm">
|
||||
<Icons.Trash />
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ export function UserDeleteForm({ userId, username, onSave, onClose }) {
|
|||
|
||||
return (
|
||||
<ConfirmationForm
|
||||
message={formatMessage(messages.confirmDelete, { target: <b>{username}</b> })}
|
||||
message={formatMessage(messages.confirmDelete, {
|
||||
target: <b key={messages.confirmDelete.id}>{username}</b>,
|
||||
})}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={onClose}
|
||||
buttonLabel={formatMessage(labels.delete)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import Link from 'next/link';
|
|||
import { formatDistance } from 'date-fns';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
import { useMessages, useLocale } from '@/components/hooks';
|
||||
import { UserDeleteButton } from './UserDeleteButton';
|
||||
import UserDeleteButton from './UserDeleteButton';
|
||||
import LinkButton from '@/components/common/LinkButton';
|
||||
|
||||
export function UsersTable({
|
||||
data = [],
|
||||
|
|
@ -44,7 +45,7 @@ export function UsersTable({
|
|||
<Row gap="3">
|
||||
<UserDeleteButton userId={id} username={username} />
|
||||
<Button asChild>
|
||||
<Link href={`/settings/users/${id}`}>
|
||||
<Link href={`/settings/users/${id}`} data-test="link-button-edit">
|
||||
<Icon>
|
||||
<Icons.Edit />
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
|||
return (
|
||||
<Form onSubmit={handleSubmit} error={getMessage(error)} values={user} style={{ width: 300 }}>
|
||||
<FormField name="username" label={formatMessage(labels.username)}>
|
||||
<TextField />
|
||||
<TextField data-test="input-username" />
|
||||
</FormField>
|
||||
<FormField
|
||||
name="password"
|
||||
|
|
@ -56,7 +56,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
|||
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
|
||||
}}
|
||||
>
|
||||
<PasswordField autoComplete="new-password" />
|
||||
<PasswordField autoComplete="new-password" data-test="input-password" />
|
||||
</FormField>
|
||||
|
||||
{user.id !== login.id && (
|
||||
|
|
@ -66,14 +66,14 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
|||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<Select defaultSelectedKey={user.role}>
|
||||
<ListItem id={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</ListItem>
|
||||
<ListItem id={ROLES.user}>{formatMessage(labels.user)}</ListItem>
|
||||
<ListItem id={ROLES.admin}>{formatMessage(labels.admin)}</ListItem>
|
||||
<ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">{formatMessage(labels.viewOnly)}</ListItem>
|
||||
<ListItem id={ROLES.user} data-test="dropdown-item-user">{formatMessage(labels.user)}</ListItem>
|
||||
<ListItem id={ROLES.admin} data-test="dropdown-item-admin">{formatMessage(labels.admin)}</ListItem>
|
||||
</Select>
|
||||
</FormField>
|
||||
)}
|
||||
<FormButtons>
|
||||
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
|
||||
<FormSubmitButton data-test="button-submit" variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function TeamMemberRemoveButton({
|
|||
{({ close }) => (
|
||||
<ConfirmationForm
|
||||
message={formatMessage(messages.confirmRemove, {
|
||||
target: <b key="username">{userName}</b>,
|
||||
target: <b key={messages.confirmRemove.id}>{userName}</b>,
|
||||
})}
|
||||
isLoading={isPending}
|
||||
error={error}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ export async function POST(request: Request) {
|
|||
const schema = z.object({
|
||||
...reportParms,
|
||||
steps: z.coerce.number().min(3).max(7),
|
||||
startStep: z.string(),
|
||||
endStep: z.string(),
|
||||
startStep: z.string().optional(),
|
||||
endStep: z.string().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { createToken, parseToken } from '@/lib/jwt';
|
|||
import { secret, uuid, hash } from '@/lib/crypto';
|
||||
import { COLLECTION_TYPE } from '@/lib/constants';
|
||||
import { anyObjectParam, urlOrPathParam } from '@/lib/schema';
|
||||
import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url';
|
||||
import { createSession, saveEvent, saveSessionData } from '@/queries';
|
||||
|
||||
const schema = z.object({
|
||||
|
|
@ -168,12 +169,12 @@ export async function POST(request: Request) {
|
|||
websiteId,
|
||||
sessionId,
|
||||
visitId,
|
||||
urlPath,
|
||||
urlPath: safeDecodeURI(urlPath),
|
||||
urlQuery,
|
||||
referrerPath,
|
||||
referrerPath: safeDecodeURI(referrerPath),
|
||||
referrerQuery,
|
||||
referrerDomain,
|
||||
pageTitle: title,
|
||||
pageTitle: safeDecodeURIComponent(title),
|
||||
eventName: name,
|
||||
eventData: data,
|
||||
hostname: hostname || urlDomain,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export function ConfirmationForm({
|
|||
<Row marginY="4">{message}</Row>
|
||||
<FormButtons>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<FormSubmitButton isLoading={isLoading} variant={buttonVariant}>
|
||||
<FormSubmitButton data-test="button-confirm" isLoading={isLoading} variant={buttonVariant}>
|
||||
{buttonLabel || formatMessage(labels.ok)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function TypeConfirmationForm({
|
|||
<Form onSubmit={onConfirm} error={error}>
|
||||
<p>
|
||||
{formatMessage(messages.actionConfirmation, {
|
||||
confirmation: <b key="value">{confirmationValue}</b>,
|
||||
confirmation: <b key={messages.actionConfirmation.id}>{confirmationValue}</b>,
|
||||
})}
|
||||
</p>
|
||||
<FormField
|
||||
|
|
|
|||
|
|
@ -69,11 +69,10 @@ export function ReferrersTable({ allowFilter, ...props }: ReferrersTableProps) {
|
|||
if (!groups[domain]) {
|
||||
groups[domain] = 0;
|
||||
}
|
||||
groups[domain] += y;
|
||||
} else {
|
||||
groups._other += y;
|
||||
groups[domain] += +y;
|
||||
}
|
||||
}
|
||||
groups._other += +y;
|
||||
}
|
||||
|
||||
return Object.keys(groups)
|
||||
|
|
|
|||
|
|
@ -397,6 +397,14 @@ export const PAID_AD_PARAMS = [
|
|||
'epik=',
|
||||
'ttclid=',
|
||||
'scid=',
|
||||
'aid=',
|
||||
'pc_id=',
|
||||
'ad_id=',
|
||||
'rdt_cid=',
|
||||
'ob_click_id=',
|
||||
'utm_medium=cpc',
|
||||
'utm_medium=paid',
|
||||
'utm_medium=paid_social',
|
||||
];
|
||||
|
||||
export const GROUPED_DOMAINS = [
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ export async function getClientInfo(request: Request, payload: Record<string, an
|
|||
const userAgent = payload?.userAgent || request.headers.get('user-agent');
|
||||
const ip = payload?.ip || getIpAddress(request.headers);
|
||||
const location = await getLocation(ip, request.headers, !!payload?.ip);
|
||||
const country = payload?.userAgent || location?.country;
|
||||
const country = location?.country;
|
||||
const subdivision1 = location?.subdivision1;
|
||||
const subdivision2 = location?.subdivision2;
|
||||
const city = location?.city;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ZodSchema } from 'zod';
|
||||
import { z, ZodSchema } from 'zod';
|
||||
import { FILTER_COLUMNS } from '@/lib/constants';
|
||||
import { badRequest, unauthorized } from '@/lib/response';
|
||||
import { getAllowedUnits, getMinimumUnit } from '@/lib/date';
|
||||
|
|
@ -24,12 +24,21 @@ export async function parseRequest(
|
|||
let error: () => void | undefined;
|
||||
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) {
|
||||
const isGet = request.method === 'GET';
|
||||
const result = schema.safeParse(isGet ? query : body);
|
||||
|
||||
if (!result.success) {
|
||||
error = () => badRequest(result.error);
|
||||
error = () => badRequest(getErrorMessages(result.error));
|
||||
} else if (isGet) {
|
||||
query = result.data;
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue