Merge branch 'dev' into patch-1

This commit is contained in:
Mike Cao 2022-02-11 20:31:48 -08:00 committed by GitHub
commit e7a6787046
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 295 additions and 1155 deletions

View File

@ -10,7 +10,7 @@ import { dateFormat } from 'lib/date';
import Calendar from 'assets/calendar-alt.svg';
import Icon from './Icon';
const filterOptions = [
export const filterOptions = [
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
{
label: (
@ -59,7 +59,7 @@ const filterOptions = [
},
];
function DateFilter({ value, startDate, endDate, onChange, className }) {
function DateFilter({ value, startDate, endDate, onChange, className, options }) {
const [showPicker, setShowPicker] = useState(false);
const displayValue =
value === 'custom' ? (
@ -86,7 +86,7 @@ function DateFilter({ value, startDate, endDate, onChange, className }) {
<DropDown
className={className}
value={displayValue}
options={filterOptions}
options={options || filterOptions}
onChange={handleChange}
/>
{showPicker && (

View File

@ -13,6 +13,8 @@ import Icon from 'components/common/Icon';
import Logo from 'assets/logo.svg';
import styles from './LoginForm.module.css';
import usePost from 'hooks/usePost';
import { setItem } from 'lib/web';
import { AUTH_TOKEN } from '../../lib/constants';
const validate = ({ username, password }) => {
const errors = {};
@ -39,6 +41,8 @@ export default function LoginForm() {
});
if (ok) {
setItem(AUTH_TOKEN, data.token);
return router.push('/');
} else {
setMessage(

View File

@ -7,15 +7,15 @@ import { TOKEN_HEADER } from 'lib/constants';
import useShareToken from 'hooks/useShareToken';
import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, className }) {
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
const shareToken = useShareToken();
const { data } = useFetch(`/api/website/${websiteId}/active`, {
interval: 60000,
const { data } = useFetch(!value && `/api/website/${websiteId}/active`, {
interval,
headers: { [TOKEN_HEADER]: shareToken?.token },
});
const count = useMemo(() => {
return data?.[0]?.x || 0;
}, [data]);
return value || data?.[0]?.x || 0;
}, [data, value]);
if (count === 0) {
return null;

View File

@ -1,7 +1,8 @@
import React from 'react';
import React, { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import PageHeader from '../layout/PageHeader';
import DropDown from '../common/DropDown';
import { differenceInMinutes } from 'date-fns';
import PageHeader from 'components/layout/PageHeader';
import DropDown from 'components/common/DropDown';
import ActiveUsers from './ActiveUsers';
import MetricCard from './MetricCard';
import styles from './RealtimeHeader.module.css';
@ -19,6 +20,12 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
const { pageviews, sessions, events, countries } = data;
const count = useMemo(() => {
return sessions.filter(
({ created_at }) => differenceInMinutes(new Date(), new Date(created_at)) <= 5,
).length;
}, [sessions]);
return (
<>
<PageHeader>
@ -26,7 +33,7 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
</div>
<div>
<ActiveUsers className={styles.active} websiteId={websiteId} />
<ActiveUsers className={styles.active} value={count} />
</div>
<DropDown value={websiteId} options={options} onChange={onSelect} />
</PageHeader>

View File

@ -2,3 +2,9 @@
display: flex;
margin-bottom: 10px;
}
@media only screen and (max-width: 576px) {
.active {
display: none;
}
}

View File

@ -65,7 +65,7 @@ export default function WebsiteChart({
};
}
return { pageviews: [], sessions: [] };
}, [data]);
}, [data, startDate, endDate, unit]);
function handleCloseFilter(param) {
router.push(resolve({ [param]: undefined }));
@ -77,8 +77,10 @@ export default function WebsiteChart({
if (ok) {
setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) });
}
} else {
} else if (typeof value === 'string') {
setDateRange(getDateRange(value, locale));
} else {
setDateRange(value);
}
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import DateFilter from 'components/common/DateFilter';
import DateFilter, { filterOptions } from 'components/common/DateFilter';
import Button from 'components/common/Button';
import useDateRange from 'hooks/useDateRange';
import { DEFAULT_DATE_RANGE } from 'lib/constants';
@ -12,6 +12,7 @@ export default function DateRangeSetting() {
const { locale } = useLocale();
const [dateRange, setDateRange] = useDateRange();
const { startDate, endDate, value } = dateRange;
const options = filterOptions.filter(e => e.value !== 'all');
function handleReset() {
setDateRange(getDateRange(DEFAULT_DATE_RANGE, locale));
@ -19,7 +20,13 @@ export default function DateRangeSetting() {
return (
<>
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={setDateRange} />
<DateFilter
options={options}
value={value}
startDate={startDate}
endDate={endDate}
onChange={setDateRange}
/>
<Button className={styles.button} size="small" onClick={handleReset}>
<FormattedMessage id="label.reset" defaultMessage="Reset" />
</Button>

View File

@ -7,6 +7,8 @@ import Icon from 'components/common/Icon';
import User from 'assets/user.svg';
import Chevron from 'assets/chevron-down.svg';
import styles from './UserButton.module.css';
import { removeItem } from 'lib/web';
import { AUTH_TOKEN } from 'lib/constants';
export default function UserButton() {
const user = useSelector(state => state.user);
@ -30,7 +32,8 @@ export default function UserButton() {
function handleSelect(value) {
if (value === 'logout') {
router.push('/logout');
removeItem(AUTH_TOKEN);
router.push('/login');
} else if (value === 'profile') {
router.push('/settings/profile');
}

View File

@ -11,13 +11,14 @@ export default function useFetch(url, options = {}, update = []) {
const [loading, setLoadiing] = useState(false);
const [count, setCount] = useState(0);
const { basePath } = useRouter();
const { params = {}, disabled, headers, delay = 0, interval, onDataLoad } = options;
const { params = {}, headers = {}, disabled, delay = 0, interval, onDataLoad } = options;
async function loadData(params) {
try {
setLoadiing(true);
setError(null);
const time = performance.now();
const { data, status, ok } = await get(`${basePath}${url}`, params, headers);
dispatch(updateQuery({ url, time: performance.now() - time, completed: Date.now() }));

View File

@ -97,7 +97,7 @@
"metrics.filter.combined": "Kombiniert",
"metrics.filter.domain-only": "Nur diese Domain",
"metrics.filter.raw": "Rohdaten",
"metrics.languages": "Languages",
"metrics.languages": "Sprachen",
"metrics.operating-systems": "Betriebssysteme",
"metrics.page-views": "Seitenaufrufe",
"metrics.pages": "Seiten",

View File

@ -5,7 +5,7 @@
"label.administrator": "Administrador",
"label.all": "Todos",
"label.all-events": "Todos los eventos",
"label.all-time": "All time",
"label.all-time": "Todos los tiempos",
"label.all-websites": "Todos los sitios",
"label.back": "Atrás",
"label.cancel": "Cancelar",
@ -36,16 +36,16 @@
"label.more": "Más",
"label.name": "Nombre",
"label.new-password": "Nueva contraseña",
"label.owner": "Owner",
"label.owner": "Propietario",
"label.password": "Contraseña",
"label.passwords-dont-match": "Las contraseñas no coinciden",
"label.profile": "Perfil",
"label.realtime": "Tiempo real",
"label.realtime-logs": "Registros en tiempo real",
"label.refresh": "Actualizar",
"label.required": "Requerido",
"label.required": "Obligatorio",
"label.reset": "Reiniciar",
"label.reset-website": "Reset statistics",
"label.reset-website": "Reiniciar estadísticas",
"label.save": "Guardar",
"label.settings": "Configuraciones",
"label.share-url": "Compartir URL",
@ -62,8 +62,8 @@
"label.websites": "Sitios",
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
"message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Copiado!",
"message.confirm-reset": "¿Seguro que deseas restablecer las estadisticas de {target}?",
"message.copied": "¡Copiado!",
"message.delete-warning": "Toda la información relacionada será eliminada.",
"message.failure": "Algo falló.",
"message.get-share-url": "Obtener URL para compartir",
@ -71,33 +71,33 @@
"message.go-to-settings": "Ir a la configuración",
"message.incorrect-username-password": "Nombre de usuario o contraseña incorrectos.",
"message.log.visitor": "Visitante desde {country} usando {browser} en {os} {device}",
"message.new-version-available": "Una nueva versíon de umami {version} esta disponible!",
"message.new-version-available": "¡Una nueva versíon de umami {version} esta disponible!",
"message.no-data-available": "Sin información disponible.",
"message.no-websites-configured": "No tienes ningún sitio configurado.",
"message.page-not-found": "Page not found",
"message.page-not-found": "Página no encontrada",
"message.powered-by": "Desarrollado con {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.reset-warning": "Todas las estadísticas de esta página serán eliminadas, pero el código de rastreo permanecerá intacto.",
"message.save-success": "Guardado exitosamente.",
"message.share-url": "Esta es la URL compartida públicamente para {target}.",
"message.toggle-charts": "Toggle charts",
"message.toggle-charts": "Alternar gráficas",
"message.track-stats": "Para registrar estadísticas para {target}, copia el siguiente código dentro de la etiqueta {head} de tu sitio.",
"message.type-delete": "Escribe {delete} abajo para confirmar.",
"message.type-reset": "Type {reset} in the box below to confirm.",
"message.type-reset": "Escribe {reset} en la caja inferior para confirmar.",
"metrics.actions": "Acciones",
"metrics.average-visit-time": "Tiempo promedio de visita",
"metrics.bounce-rate": "Porcentaje de rebote",
"metrics.browsers": "Navegadores",
"metrics.countries": "Países",
"metrics.device.desktop": "Desktop",
"metrics.device.laptop": "Laptop",
"metrics.device.mobile": "Mobile",
"metrics.device.desktop": "Escritorio",
"metrics.device.laptop": "Portátil",
"metrics.device.mobile": "Móvil",
"metrics.device.tablet": "Tableta",
"metrics.devices": "Dispositivos",
"metrics.events": "Eventos",
"metrics.filter.combined": "Combinado",
"metrics.filter.domain-only": "Únicamente dominio",
"metrics.filter.raw": "Personalizado",
"metrics.languages": "Languages",
"metrics.languages": "Idiomas",
"metrics.operating-systems": "Sistemas operativos",
"metrics.page-views": "Vistas",
"metrics.pages": "Páginas",

View File

@ -5,7 +5,7 @@
"label.administrator": "Адміністратор",
"label.all": "Всі",
"label.all-events": "Всі події",
"label.all-time": "All time",
"label.all-time": "Весь час",
"label.all-websites": "Всі сайти",
"label.back": "Назад",
"label.cancel": "Відмінити",
@ -36,7 +36,7 @@
"label.more": "Більше",
"label.name": "Ім'я",
"label.new-password": "Новий пароль",
"label.owner": "Owner",
"label.owner": "Власник",
"label.password": "Пароль",
"label.passwords-dont-match": "Паролі не співпадають",
"label.profile": "Профіль",
@ -45,7 +45,7 @@
"label.refresh": "Оновити",
"label.required": "Обов'язкове",
"label.reset": "Скинути",
"label.reset-website": "Reset statistics",
"label.reset-website": "Скинути статистику сайту",
"label.save": "Зберегти",
"label.settings": "Налаштування",
"label.share-url": "Поділитися посилання",
@ -62,7 +62,7 @@
"label.websites": "Сайти",
"message.active-users": "{x} поточних відвідувачів",
"message.confirm-delete": "Ви впевнені, що бажаєте видалити {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.confirm-reset": "Ви впевнені, що бажаєте скинути статистику для {target}?",
"message.copied": "Скопійовано!",
"message.delete-warning": "Усі пов'язані дані будуть видалені також.",
"message.failure": "Щось пішло не так.",
@ -76,13 +76,13 @@
"message.no-websites-configured": "У вас немає налаштованих сайтів.",
"message.page-not-found": "Сторінку не знайдено.",
"message.powered-by": "На базі {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.",
"message.reset-warning": "Вся статистика для цього сайту буде видалена, проте код відслідковування буде продовжувати працювати.",
"message.save-success": "Збережено успішно.",
"message.share-url": "Це публічне посилання для {target}.",
"message.toggle-charts": "Toggle charts",
"message.toggle-charts": "Переключити графіки",
"message.track-stats": "Аби відслідковувати статистику для {target}, розмістіть наступний код у {head} секції вашого сайту.",
"message.type-delete": "Введіть {delete} у полі нижче щоб підтвердити.",
"message.type-reset": "Type {reset} in the box below to confirm.",
"message.type-delete": "Введіть {delete} у полі нижче для підтвердження.",
"message.type-reset": "Введіть {reset} у полі нижче для підтвердження.",
"metrics.actions": "Дії",
"metrics.average-visit-time": "Середній час візиту",
"metrics.bounce-rate": "Показник відмов",
@ -97,7 +97,7 @@
"metrics.filter.combined": "Об'єднані",
"metrics.filter.domain-only": "Лише домен",
"metrics.filter.raw": "Сирі дані",
"metrics.languages": "Languages",
"metrics.languages": "Мови",
"metrics.operating-systems": "Операційні системи",
"metrics.page-views": "Перегляди сторінок",
"metrics.pages": "Сторінки",

View File

@ -1,12 +1,15 @@
import { parse } from 'cookie';
import { parseSecureToken, parseToken } from './crypto';
import { AUTH_COOKIE_NAME, TOKEN_HEADER } from './constants';
import { TOKEN_HEADER } from './constants';
import { getWebsiteById } from './queries';
export async function getAuthToken(req) {
const token = parse(req.headers.cookie || '')[AUTH_COOKIE_NAME];
try {
const token = req.headers.authorization;
return parseSecureToken(token);
return parseSecureToken(token.split(' ')[1]);
} catch {
return null;
}
}
export async function isValidToken(token, validation) {

View File

@ -1,4 +1,4 @@
export const AUTH_COOKIE_NAME = 'umami.auth';
export const AUTH_TOKEN = 'umami.auth';
export const LOCALE_CONFIG = 'umami.locale';
export const TIMEZONE_CONFIG = 'umami.timezone';
export const DATE_RANGE_CONFIG = 'umami.date-range';
@ -80,7 +80,8 @@ export const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01',
};
export const DOMAIN_REGEX = /^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/;
export const DOMAIN_REGEX =
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/;
export const DESKTOP_SCREEN_WIDTH = 1920;
export const LAPTOP_SCREEN_WIDTH = 1024;

View File

@ -1,13 +1,17 @@
import { makeUrl } from './url';
import { AUTH_TOKEN } from './constants';
export const apiRequest = (method, url, body, headers) =>
fetch(url, {
export const apiRequest = (method, url, body, headers) => {
const authToken = getItem(AUTH_TOKEN);
return fetch(url, {
method,
cache: 'no-cache',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
...headers,
},
body,
@ -18,6 +22,7 @@ export const apiRequest = (method, url, body, headers) =>
return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data }));
});
};
export const get = (url, params, headers) =>
apiRequest('get', makeUrl(url, params), undefined, headers);
@ -64,3 +69,9 @@ export const getItem = (key, session) =>
typeof window !== 'undefined'
? JSON.parse((session ? sessionStorage : localStorage).getItem(key))
: null;
export const removeItem = (key, session) => {
if (typeof window !== 'undefined') {
(session ? sessionStorage : localStorage).removeItem(key);
}
};

View File

@ -1,7 +1,7 @@
{
"name": "umami",
"version": "1.25.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
"description": "A simple, fast, website analytics alternative to Google Analytics.",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
"homepage": "https://umami.is",
@ -56,7 +56,7 @@
},
"dependencies": {
"@fontsource/inter": "4.5.0",
"@prisma/client": "3.6.0",
"@prisma/client": "3.8.1",
"@reduxjs/toolkit": "^1.6.1",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
@ -77,7 +77,7 @@
"jose": "2.0.5",
"maxmind": "^4.3.2",
"moment-timezone": "^0.5.33",
"next": "12.0.5",
"next": "12.0.8",
"prompts": "2.4.2",
"prop-types": "^15.7.2",
"react": "^17.0.2",
@ -124,7 +124,7 @@
"postcss-rtlcss": "^3.3.2",
"prettier": "^2.3.2",
"prettier-eslint": "^13.0.0",
"prisma": "3.6.0",
"prisma": "3.8.1",
"rollup": "^2.48.0",
"rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.2",

View File

@ -1,7 +1,5 @@
import { serialize } from 'cookie';
import { checkPassword, createSecureToken } from 'lib/crypto';
import { getAccountByUsername } from 'lib/queries';
import { AUTH_COOKIE_NAME } from 'lib/constants';
import { ok, unauthorized, badRequest } from 'lib/response';
export default async (req, res) => {
@ -16,14 +14,6 @@ export default async (req, res) => {
if (account && (await checkPassword(password, account.password))) {
const { user_id, username, is_admin } = account;
const token = await createSecureToken({ user_id, username, is_admin });
const cookie = serialize(AUTH_COOKIE_NAME, token, {
path: '/',
httpOnly: true,
sameSite: true,
maxAge: 60 * 60 * 24 * 365,
});
res.setHeader('Set-Cookie', [cookie]);
return ok(res, { token });
}

View File

@ -1,15 +0,0 @@
import { serialize } from 'cookie';
import { AUTH_COOKIE_NAME } from 'lib/constants';
import { ok } from 'lib/response';
export default async (req, res) => {
const cookie = serialize(AUTH_COOKIE_NAME, '', {
path: '/',
httpOnly: true,
maxAge: 0,
});
res.setHeader('Set-Cookie', [cookie]);
return ok(res);
};

View File

@ -114,17 +114,16 @@ import { removeTrailingSlash } from '../lib/url';
};
const addEvent = element => {
element.className &&
element.className.split(' ').forEach(className => {
if (!eventClass.test(className)) return;
(element.getAttribute('class') || '').split(' ').forEach(className => {
if (!eventClass.test(className)) return;
const [, type, value] = className.split('--');
const listener = listeners[className]
? listeners[className]
: (listeners[className] = () => trackEvent(value, type));
const [, type, value] = className.split('--');
const listener = listeners[className]
? listeners[className]
: (listeners[className] = () => trackEvent(value, type));
element.addEventListener(type, listener, true);
});
element.addEventListener(type, listener, true);
});
};
const monitorMutate = mutations => {

1253
yarn.lock

File diff suppressed because it is too large Load Diff