Merge pull request #642 from mikecao/dev

v1.17.0
This commit is contained in:
Mike Cao 2021-04-28 02:48:48 -07:00 committed by GitHub
commit 5ecaf5587b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 854 additions and 628 deletions

View File

@ -4,7 +4,7 @@
"es2020": true, "es2020": true,
"node": true "node": true
}, },
"extends": ["eslint:recommended", "plugin:react/recommended", "prettier", "prettier/react"], "extends": ["eslint:recommended", "plugin:react/recommended", "prettier"],
"parserOptions": { "parserOptions": {
"ecmaFeatures": { "ecmaFeatures": {
"jsx": true "jsx": true

1
assets/chart-bar.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Pro 5.15.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M396.8 352h22.4c6.4 0 12.8-6.4 12.8-12.8V108.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v230.4c0 6.4 6.4 12.8 12.8 12.8zm-192 0h22.4c6.4 0 12.8-6.4 12.8-12.8V140.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v198.4c0 6.4 6.4 12.8 12.8 12.8zm96 0h22.4c6.4 0 12.8-6.4 12.8-12.8V204.8c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v134.4c0 6.4 6.4 12.8 12.8 12.8zM496 400H48V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-16c0-8.84-7.16-16-16-16zm-387.2-48h22.4c6.4 0 12.8-6.4 12.8-12.8v-70.4c0-6.4-6.4-12.8-12.8-12.8h-22.4c-6.4 0-12.8 6.4-12.8 12.8v70.4c0 6.4 6.4 12.8 12.8 12.8z"/></svg>

After

Width:  |  Height:  |  Size: 885 B

View File

@ -7,12 +7,14 @@ import styles from './Checkbox.module.css';
function Checkbox({ name, value, label, onChange }) { function Checkbox({ name, value, label, onChange }) {
const ref = useRef(); const ref = useRef();
const onClick = () => ref.current.click();
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.checkbox} onClick={() => ref.current.click()}> <div className={styles.checkbox} onClick={onClick}>
{value && <Icon icon={<Check />} size="small" />} {value && <Icon icon={<Check />} size="small" />}
</div> </div>
<label className={styles.label} htmlFor={name}> <label className={styles.label} htmlFor={name} onClick={onClick}>
{label} {label}
</label> </label>
<input <input
@ -20,7 +22,7 @@ function Checkbox({ name, value, label, onChange }) {
className={styles.input} className={styles.input}
type="checkbox" type="checkbox"
name={name} name={name}
value={value} defaultChecked={value}
onChange={onChange} onChange={onChange}
/> />
</div> </div>

View File

@ -17,6 +17,7 @@
.label { .label {
margin-left: 10px; margin-left: 10px;
user-select: none; /* disable text selection when clicking to toggle the checkbox */
} }
.input { .input {

View File

@ -77,7 +77,7 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
</div> </div>
</FormRow> </FormRow>
<FormRow> <FormRow>
<label></label> <label />
<Field name="enable_share_url"> <Field name="enable_share_url">
{({ field }) => ( {({ field }) => (
<Checkbox <Checkbox

View File

@ -70,6 +70,11 @@
padding: 0 15px; padding: 0 15px;
} }
.title {
padding: 0.5rem;
margin-bottom: 0.5rem;
}
.nav { .nav {
font-size: var(--font-size-normal); font-size: var(--font-size-normal);
flex-wrap: wrap; flex-wrap: wrap;
@ -102,6 +107,9 @@
.burger { .burger {
display: block; display: block;
/* color: #4a4a4a; */ /* color: #4a4a4a; */
background: none;
border: 1px solid var(--gray900);
border-radius: 4px;
cursor: pointer; cursor: pointer;
height: 3.25rem; height: 3.25rem;
width: 3.25rem; width: 3.25rem;
@ -112,20 +120,20 @@
} }
.burger span { .burger span {
transform: translateX(-50%); transform: translateX(25%);
padding: 1px 0px; padding: 1px 0px;
margin: 6px 0; margin: 6px 0;
width: 20px; width: 65%;
display: block; display: block;
background-color: white; background-color: var(--gray900);
} }
.burger div { .burger div {
height: 100%; /* height: 100%; */
color: white; color: var(--gray900);
text-align: center; text-align: center;
margin: auto; margin: auto;
font-size: 1.5rem; font-size: 1.5rem;
transform: translateX(-50%); /* transform: translateX(-50%); */
} }
} }

View File

@ -23,6 +23,7 @@ export default function WebsiteChart({
domain, domain,
stickyHeader = false, stickyHeader = false,
showLink = false, showLink = false,
hideChart = false,
onDataLoad = () => {}, onDataLoad = () => {},
}) { }) {
const shareToken = useShareToken(); const shareToken = useShareToken();
@ -91,6 +92,7 @@ export default function WebsiteChart({
<div className="row"> <div className="row">
<div className="col"> <div className="col">
{error && <ErrorMessage />} {error && <ErrorMessage />}
{!hideChart && (
<PageviewsChart <PageviewsChart
websiteId={websiteId} websiteId={websiteId}
data={chartData} data={chartData}
@ -98,6 +100,7 @@ export default function WebsiteChart({
records={getDateLength(startDate, endDate, unit)} records={getDateLength(startDate, endDate, unit)}
loading={loading} loading={loading}
/> />
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,28 +1,26 @@
import React from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import WebsiteChart from 'components/metrics/WebsiteChart'; import WebsiteChart from 'components/metrics/WebsiteChart';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Button from 'components/common/Button';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import Arrow from 'assets/arrow-right.svg'; import Arrow from 'assets/arrow-right.svg';
import Chart from 'assets/chart-bar.svg';
import styles from './WebsiteList.module.css'; import styles from './WebsiteList.module.css';
export default function WebsiteList({ userId }) { export default function WebsiteList({ userId }) {
const { data } = useFetch('/api/websites', { params: { user_id: userId } }); const { data } = useFetch('/api/websites', { params: { user_id: userId } });
const [hideCharts, setHideCharts] = useState(false);
if (!data) { if (!data) {
return null; return null;
} }
if (data.length === 0) {
return ( return (
<Page> <Page>
{data.map(({ website_id, name, domain }) => (
<div key={website_id} className={styles.website}>
<WebsiteChart websiteId={website_id} title={name} domain={domain} showLink />
</div>
))}
{data.length === 0 && (
<EmptyPlaceholder <EmptyPlaceholder
msg={ msg={
<FormattedMessage <FormattedMessage
@ -35,7 +33,26 @@ export default function WebsiteList({ userId }) {
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" /> <FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
</Link> </Link>
</EmptyPlaceholder> </EmptyPlaceholder>
)} </Page>
);
}
return (
<Page>
<div className={styles.menubar}>
<Button icon={<Chart />} onClick={() => setHideCharts(!hideCharts)} />
</div>
{data.map(({ website_id, name, domain }) => (
<div key={website_id} className={styles.website}>
<WebsiteChart
websiteId={website_id}
title={name}
domain={domain}
hideChart={hideCharts}
showLink
/>
</div>
))}
</Page> </Page>
); );
} }

View File

@ -9,3 +9,10 @@
border-bottom: 0; border-bottom: 0;
margin-bottom: 0; margin-bottom: 0;
} }
.menubar {
display: flex;
align-items: center;
justify-content: flex-end;
padding-top: 10px;
}

View File

@ -5,7 +5,13 @@ import { THEME_CONFIG } from 'lib/constants';
import { useEffect } from 'react'; import { useEffect } from 'react';
export default function useTheme() { export default function useTheme() {
const theme = useSelector(state => state.app.theme || getItem(THEME_CONFIG) || 'light'); const defaultTheme =
typeof window !== 'undefined'
? window?.matchMedia('prefers-color-scheme: dark')?.matches
? 'dark'
: 'light'
: 'light';
const theme = useSelector(state => state.app.theme || getItem(THEME_CONFIG) || defaultTheme);
const dispatch = useDispatch(); const dispatch = useDispatch();
function saveTheme(value) { function saveTheme(value) {

View File

@ -5,6 +5,7 @@
"label.administrator": "Administrator", "label.administrator": "Administrator",
"label.all": "Alles", "label.all": "Alles",
"label.all-websites": "Alle websites", "label.all-websites": "Alle websites",
"label.all-events": "Alle gebeurtenissen",
"label.back": "Terug", "label.back": "Terug",
"label.cancel": "Annuleren", "label.cancel": "Annuleren",
"label.change-password": "Wachtwoord wijzigen", "label.change-password": "Wachtwoord wijzigen",

View File

@ -5,6 +5,7 @@
"label.administrator": "Администратор", "label.administrator": "Администратор",
"label.all": "Все", "label.all": "Все",
"label.all-websites": "Все сайты", "label.all-websites": "Все сайты",
"label.all-events": "Все события",
"label.back": "Назад", "label.back": "Назад",
"label.cancel": "Отменить", "label.cancel": "Отменить",
"label.change-password": "Изменить пароль", "label.change-password": "Изменить пароль",

View File

@ -5,6 +5,7 @@
"label.administrator": "Адміністратор", "label.administrator": "Адміністратор",
"label.all": "Всі", "label.all": "Всі",
"label.all-websites": "Всі сайти", "label.all-websites": "Всі сайти",
"label.all-events": "Всі події",
"label.back": "Назад", "label.back": "Назад",
"label.cancel": "Відмінити", "label.cancel": "Відмінити",
"label.change-password": "Змінити пароль", "label.change-password": "Змінити пароль",

View File

@ -1,6 +1,6 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { v4, v5, validate } from 'uuid'; import { v4, v5, validate } from 'uuid';
import bcrypt from 'bcrypt'; import bcrypt from 'bcryptjs';
import { JWT, JWE, JWK } from 'jose'; import { JWT, JWE, JWK } from 'jose';
import { startOfMonth } from 'date-fns'; import { startOfMonth } from 'date-fns';
@ -40,11 +40,11 @@ export function getRandomChars(n) {
} }
export async function hashPassword(password) { export async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS); return bcrypt.hashSync(password, SALT_ROUNDS);
} }
export async function checkPassword(password, hash) { export async function checkPassword(password, hash) {
return bcrypt.compare(password, hash); return bcrypt.compareSync(password, hash);
} }
export async function createToken(payload) { export async function createToken(payload) {

View File

@ -18,7 +18,7 @@ export const urlFilter = (data, { raw }) => {
return `${pathname}${search}`; return `${pathname}${search}`;
} }
return removeTrailingSlash(pathname); return pathname;
} catch { } catch {
return null; return null;
} }

View File

@ -12,6 +12,8 @@ import {
MOBILE_SCREEN_WIDTH, MOBILE_SCREEN_WIDTH,
} from './constants'; } from './constants';
let lookup;
export function getIpAddress(req) { export function getIpAddress(req) {
// Cloudflare // Cloudflare
if (req.headers['cf-connecting-ip']) { if (req.headers['cf-connecting-ip']) {
@ -61,7 +63,9 @@ export async function getCountry(req, ip) {
} }
// Database lookup // Database lookup
const lookup = await maxmind.open(path.resolve('./public/geo/GeoLite2-Country.mmdb')); if (!lookup) {
lookup = await maxmind.open(path.resolve('./public/geo/GeoLite2-Country.mmdb'));
}
const result = lookup.get(ip); const result = lookup.get(ip);

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "1.16.0", "version": "1.17.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",
@ -56,12 +56,12 @@
} }
}, },
"dependencies": { "dependencies": {
"@prisma/client": "2.19.0", "@prisma/client": "2.21.2",
"@reduxjs/toolkit": "^1.5.0", "@reduxjs/toolkit": "^1.5.1",
"bcrypt": "^5.0.0", "bcryptjs": "^2.4.3",
"chalk": "^4.1.0", "chalk": "^4.1.1",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"classnames": "^2.2.6", "classnames": "^2.3.1",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"date-fns": "^2.16.1", "date-fns": "^2.16.1",
@ -70,27 +70,28 @@
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"formik": "^2.2.6", "formik": "^2.2.6",
"immer": "^8.0.1", "immer": "^8.0.1",
"ipaddr.js": "^2.0.0",
"is-localhost-ip": "^1.4.0", "is-localhost-ip": "^1.4.0",
"isbot": "^3.0.25", "isbot": "^3.0.26",
"jose": "2.0.3", "jose": "2.0.5",
"maxmind": "^4.3.1", "maxmind": "^4.3.1",
"moment-timezone": "^0.5.32", "moment-timezone": "^0.5.33",
"next": "^10.0.9", "next": "^10.1.3",
"prompts": "2.4.0", "prompts": "2.4.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-intl": "^5.14.1", "react-intl": "^5.16.0",
"react-redux": "^7.2.3", "react-redux": "^7.2.4",
"react-simple-maps": "^2.3.0", "react-simple-maps": "^2.3.0",
"react-spring": "^8.0.27", "react-spring": "^8.0.27",
"react-tooltip": "^4.2.17", "react-tooltip": "^4.2.18",
"react-use-measure": "^2.0.4", "react-use-measure": "^2.0.4",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"redux": "^4.0.5", "redux": "^4.1.0",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"request-ip": "^2.1.3", "request-ip": "^2.1.3",
"semver": "^7.3.4", "semver": "^7.3.5",
"thenby": "^1.3.4", "thenby": "^1.3.4",
"timezone-support": "^2.0.2", "timezone-support": "^2.0.2",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
@ -99,16 +100,16 @@
"devDependencies": { "devDependencies": {
"@formatjs/cli": "^2.13.16", "@formatjs/cli": "^2.13.16",
"@rollup/plugin-buble": "^0.21.3", "@rollup/plugin-buble": "^0.21.3",
"@rollup/plugin-node-resolve": "^11.1.1", "@rollup/plugin-node-resolve": "^11.2.1",
"@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-replace": "^2.3.4",
"@svgr/webpack": "^5.5.0", "@svgr/webpack": "^5.5.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"del": "^6.0.0", "del": "^6.0.0",
"dotenv-cli": "^4.0.0", "dotenv-cli": "^4.0.0",
"eslint": "^7.20.0", "eslint": "^7.25.0",
"eslint-config-prettier": "^7.2.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"extract-react-intl-messages": "^4.1.1", "extract-react-intl-messages": "^4.1.1",
"husky": "^4.3.8", "husky": "^4.3.8",
@ -120,14 +121,14 @@
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"prettier-eslint": "^12.0.0", "prettier-eslint": "^12.0.0",
"prisma": "2.19.0", "prisma": "2.21.2",
"rollup": "^2.38.3", "rollup": "^2.45.2",
"rollup-plugin-hashbang": "^2.2.2", "rollup-plugin-hashbang": "^2.2.2",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"stylelint": "^13.10.0", "stylelint": "^13.13.0",
"stylelint-config-css-modules": "^2.2.0", "stylelint-config-css-modules": "^2.2.0",
"stylelint-config-prettier": "^8.0.1", "stylelint-config-prettier": "^8.0.1",
"stylelint-config-recommended": "^3.0.0", "stylelint-config-recommended": "^5.0.0",
"tar": "^6.0.5" "tar": "^6.0.5"
} }
} }

View File

@ -1,4 +1,5 @@
import isbot from 'isbot'; import isbot from 'isbot';
import ipaddr from 'ipaddr.js';
import { savePageView, saveEvent } from 'lib/queries'; import { savePageView, saveEvent } from 'lib/queries';
import { useCors, useSession } from 'lib/middleware'; import { useCors, useSession } from 'lib/middleware';
import { getIpAddress } from 'lib/request'; import { getIpAddress } from 'lib/request';
@ -15,8 +16,21 @@ export default async (req, res) => {
if (process.env.IGNORE_IP) { if (process.env.IGNORE_IP) {
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim()); const ips = process.env.IGNORE_IP.split(',').map(n => n.trim());
const ip = getIpAddress(req); const ip = getIpAddress(req);
const blocked = ips.find(i => {
if (i === ip) return true;
if (ips.includes(ip)) { // CIDR notation
if (i.indexOf('/') > 0) {
const addr = ipaddr.parse(ip);
const range = ipaddr.parseCIDR(i);
if (addr.match(range)) return true;
}
return false;
});
if (blocked) {
return ok(res); return ok(res);
} }
} }

1295
yarn.lock

File diff suppressed because it is too large Load Diff