Merge branch 'dev' into dependabot/npm_and_yarn/nanoid-3.2.0

This commit is contained in:
Mike Cao 2022-01-24 22:49:20 -08:00 committed by GitHub
commit 3339ac19a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 278 additions and 1138 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -2,3 +2,9 @@
display: flex; display: flex;
margin-bottom: 10px; 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: [] }; return { pageviews: [], sessions: [] };
}, [data]); }, [data, startDate, endDate, unit]);
function handleCloseFilter(param) { function handleCloseFilter(param) {
router.push(resolve({ [param]: undefined })); router.push(resolve({ [param]: undefined }));
@ -77,8 +77,10 @@ export default function WebsiteChart({
if (ok) { if (ok) {
setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) }); setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) });
} }
} else { } else if (typeof value === 'string') {
setDateRange(getDateRange(value, locale)); setDateRange(getDateRange(value, locale));
} else {
setDateRange(value);
} }
} }

View File

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

View File

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

View File

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

View File

@ -62,7 +62,7 @@
"label.websites": "Webseiten", "label.websites": "Webseiten",
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}", "message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
"message.confirm-delete": "Sind Sie sich sicher {target} zu löschen?", "message.confirm-delete": "Sind Sie sich sicher {target} zu löschen?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", "message.confirm-reset": "Sind Sie sich sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?",
"message.copied": "In Zwischenablage kopiert!", "message.copied": "In Zwischenablage kopiert!",
"message.delete-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.", "message.delete-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.",
"message.failure": "Es ist ein Fehler aufgetreten.", "message.failure": "Es ist ein Fehler aufgetreten.",
@ -97,7 +97,7 @@
"metrics.filter.combined": "Kombiniert", "metrics.filter.combined": "Kombiniert",
"metrics.filter.domain-only": "Nur diese Domain", "metrics.filter.domain-only": "Nur diese Domain",
"metrics.filter.raw": "Rohdaten", "metrics.filter.raw": "Rohdaten",
"metrics.languages": "Languages", "metrics.languages": "Sprachen",
"metrics.operating-systems": "Betriebssysteme", "metrics.operating-systems": "Betriebssysteme",
"metrics.page-views": "Seitenaufrufe", "metrics.page-views": "Seitenaufrufe",
"metrics.pages": "Seiten", "metrics.pages": "Seiten",

View File

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

View File

@ -1,12 +1,15 @@
import { parse } from 'cookie';
import { parseSecureToken, parseToken } from './crypto'; import { parseSecureToken, parseToken } from './crypto';
import { AUTH_COOKIE_NAME, TOKEN_HEADER } from './constants'; import { TOKEN_HEADER } from './constants';
import { getWebsiteById } from './queries'; import { getWebsiteById } from './queries';
export async function getAuthToken(req) { 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) { 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 LOCALE_CONFIG = 'umami.locale';
export const TIMEZONE_CONFIG = 'umami.timezone'; export const TIMEZONE_CONFIG = 'umami.timezone';
export const DATE_RANGE_CONFIG = 'umami.date-range'; export const DATE_RANGE_CONFIG = 'umami.date-range';
@ -80,7 +80,8 @@ export const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01', 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 DESKTOP_SCREEN_WIDTH = 1920;
export const LAPTOP_SCREEN_WIDTH = 1024; export const LAPTOP_SCREEN_WIDTH = 1024;

View File

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

View File

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

View File

@ -1,7 +1,5 @@
import { serialize } from 'cookie';
import { checkPassword, createSecureToken } from 'lib/crypto'; import { checkPassword, createSecureToken } from 'lib/crypto';
import { getAccountByUsername } from 'lib/queries'; import { getAccountByUsername } from 'lib/queries';
import { AUTH_COOKIE_NAME } from 'lib/constants';
import { ok, unauthorized, badRequest } from 'lib/response'; import { ok, unauthorized, badRequest } from 'lib/response';
export default async (req, res) => { export default async (req, res) => {
@ -16,14 +14,6 @@ export default async (req, res) => {
if (account && (await checkPassword(password, account.password))) { if (account && (await checkPassword(password, account.password))) {
const { user_id, username, is_admin } = account; const { user_id, username, is_admin } = account;
const token = await createSecureToken({ user_id, username, is_admin }); 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 }); 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 => { const addEvent = element => {
element.className && (element.getAttribute('class') || '').split(' ').forEach(className => {
element.className.split(' ').forEach(className => { if (!eventClass.test(className)) return;
if (!eventClass.test(className)) return;
const [, type, value] = className.split('--'); const [, type, value] = className.split('--');
const listener = listeners[className] const listener = listeners[className]
? listeners[className] ? listeners[className]
: (listeners[className] = () => trackEvent(value, type)); : (listeners[className] = () => trackEvent(value, type));
element.addEventListener(type, listener, true); element.addEventListener(type, listener, true);
}); });
}; };
const monitorMutate = mutations => { const monitorMutate = mutations => {

1247
yarn.lock

File diff suppressed because it is too large Load Diff