Merge branch 'mikecao:master' into master

This commit is contained in:
Didier Krux 2022-06-27 11:37:46 +01:00 committed by GitHub
commit d7144ee485
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 2206 additions and 1086 deletions

36
.github/workflows/cd-manual.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Create docker images
on:
workflow_dispatch:
inputs:
version:
type: string
description: Version
required: true
add-latest:
type: boolean
description: Add latest tag
required: false
jobs:
build:
name: Build, push, and deploy
runs-on: ubuntu-latest
strategy:
matrix:
db-type: [postgresql, mysql]
steps:
- uses: actions/checkout@v2
- uses: mr-smithers-excellent/docker-build-push@v5
name: Build & push Docker image for ${{ matrix.db-type }}
with:
image: umami
tags: ${{ matrix.db-type }}-${{ inputs.version }}
addLatest: ${{ inputs.add-latest }}
buildArgs: DATABASE_TYPE=${{ matrix.db-type }}
registry: ghcr.io/${{ github.actor }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View file

@ -11,7 +11,7 @@
# next.js # next.js
/.next/ /.next/
/out/ /out/
/prisma/schema.prisma /prisma/
# production # production
/build /build

View file

@ -1,45 +1,59 @@
# Build image # Install dependencies only when needed
FROM node:12.22-alpine AS build FROM node:16-alpine AS deps
ARG BASE_PATH # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
ARG DATABASE_TYPE RUN apk add --no-cache libc6-compat
WORKDIR /app
ENV BASE_PATH=$BASE_PATH COPY package.json yarn.lock ./
ENV DATABASE_URL "postgresql://umami:umami@db:5432/umami"
ENV DATABASE_TYPE=$DATABASE_TYPE
WORKDIR /build
RUN yarn config set --home enableTelemetry 0
COPY package.json yarn.lock /build/
# Install only the production dependencies
RUN yarn install --production --frozen-lockfile
# Cache these modules for production
RUN cp -R node_modules/ prod_node_modules/
# Install development dependencies
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
COPY . /build # Rebuild the source code only when needed
RUN yarn next telemetry disable FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ARG DATABASE_URL
ARG DATABASE_TYPE
ARG BASE_PATH
ARG DISABLE_LOGIN
ENV DATABASE_URL $DATABASE_URL
ENV DATABASE_TYPE $DATABASE_TYPE
ENV BASE_PATH $BASE_PATH
ENV DISABLE_LOGIN $DISABLE_LOGIN
ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build RUN yarn build
# Production image # Production image, copy all the files and run next
FROM node:12.22-alpine AS production FROM node:16-alpine AS runner
WORKDIR /app WORKDIR /app
# Copy cached dependencies ENV NODE_ENV production
COPY --from=build /build/prod_node_modules ./node_modules ENV NEXT_TELEMETRY_DISABLED 1
# Copy generated Prisma client RUN addgroup --system --gid 1001 nodejs
COPY --from=build /build/node_modules/.prisma/ ./node_modules/.prisma/ RUN adduser --system --uid 1001 nextjs
COPY --from=build /build/yarn.lock /build/package.json ./ RUN yarn global add prisma
COPY --from=build /build/.next ./.next
COPY --from=build /build/public ./public
USER node # You only need to copy next.config.js if you are NOT using the default configuration
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/prisma/schema.prisma ./prisma/schema.prisma
COPY --from=builder /app/prisma/migrations ./prisma/migrations
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000 EXPOSE 3000
CMD ["yarn", "start"]
ENV PORT 3000
CMD ["yarn", "start-docker"]

View file

@ -10,43 +10,29 @@ A detailed getting started guide can be found at [https://umami.is/docs/](https:
### Requirements ### Requirements
- A server with Node.js 12 or newer - A server with Node.js version 12 or newer
- A database (MySQL or Postgresql) - A database. Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/) databases.
### Install Yarn
```
npm install -g yarn
```
### Get the source code and install packages ### Get the source code and install packages
``` ```
git clone https://github.com/mikecao/umami.git git clone https://github.com/mikecao/umami.git
cd umami cd umami
npm install yarn install
``` ```
### Create database tables
Umami supports [MySQL](https://www.mysql.com/) and [Postgresql](https://www.postgresql.org/).
Create a database for your Umami installation and install the tables with the included scripts.
For MySQL:
```
mysql -u username -p databasename < sql/schema.mysql.sql
```
For Postgresql:
```
psql -h hostname -U username -d databasename -f sql/schema.postgresql.sql
```
This will also create a login account with username **admin** and password **umami**.
### Configure umami ### Configure umami
Create an `.env` file with the following Create an `.env` file with the following
``` ```
DATABASE_URL=(connection url) DATABASE_URL=(connection url)
HASH_SALT=(any random string)
``` ```
The connection url is in the following format: The connection url is in the following format:
@ -56,18 +42,24 @@ postgresql://username:mypassword@localhost:5432/mydb
mysql://username:mypassword@localhost:3306/mydb mysql://username:mypassword@localhost:3306/mydb
``` ```
The `HASH_SALT` is used to generate unique values for your installation.
### Build the application ### Build the application
```bash ```bash
npm run build yarn build
``` ```
### Create database tables
```bash
yarn update-db
```
This will also create a login account with username **admin** and password **umami**.
### Start the application ### Start the application
```bash ```bash
npm start yarn start
``` ```
By default this will launch the application on `http://localhost:3000`. You will need to either By default this will launch the application on `http://localhost:3000`. You will need to either
@ -98,8 +90,9 @@ To get the latest features, simply do a pull, install any new dependencies, and
```bash ```bash
git pull git pull
npm install yarn install
npm run build yarn build
yarn update-db
``` ```
To update the Docker image, simply pull the new images and rebuild: To update the Docker image, simply pull the new images and rebuild:

View file

@ -1,17 +1,10 @@
{ {
"name": "Umami", "name": "Umami",
"description": "Umami is a simple, fast, website analytics alternative to Google Analytics.", "description": "Umami is a simple, fast, website analytics alternative to Google Analytics.",
"keywords": [ "keywords": ["analytics", "charts", "statistics", "web-analytics"],
"analytics",
"charts",
"statistics",
"web-analytics"
],
"website": "https://umami.is", "website": "https://umami.is",
"repository": "https://github.com/mikecao/umami", "repository": "https://github.com/mikecao/umami",
"addons": [ "addons": ["heroku-postgresql"],
"heroku-postgresql"
],
"env": { "env": {
"HASH_SALT": { "HASH_SALT": {
"description": "Used to generate unique values for your installation", "description": "Used to generate unique values for your installation",
@ -19,8 +12,5 @@
"generator": "secret" "generator": "secret"
} }
}, },
"scripts": {
"postdeploy": "psql $DATABASE_URL -f sql/schema.postgresql.sql"
},
"success_url": "/" "success_url": "/"
} }

View file

@ -1,27 +1,38 @@
import React from 'react'; import { useState, useEffect, useCallback } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import useVersion from 'hooks/useVersion'; import ButtonLayout from 'components/layout/ButtonLayout';
import styles from './UpdateNotice.module.css'; import useStore, { checkVersion } from 'store/version';
import ButtonLayout from '../layout/ButtonLayout'; import { setItem } from 'lib/web';
import { VERSION_CHECK, VERSION_URL } from 'lib/constants';
import Button from './Button'; import Button from './Button';
import useForceUpdate from '../../hooks/useForceUpdate'; import styles from './UpdateNotice.module.css';
export default function UpdateNotice() { export default function UpdateNotice() {
const forceUpdate = useForceUpdate(); const { latest, checked, hasUpdate } = useStore();
const { hasUpdate, checked, latest, updateCheck } = useVersion(true); const [dismissed, setDismissed] = useState(false);
const updateCheck = useCallback(() => {
setItem(VERSION_CHECK, { version: latest, time: Date.now() });
}, [latest]);
function handleViewClick() { function handleViewClick() {
location.href = 'https://github.com/mikecao/umami/releases';
updateCheck(); updateCheck();
forceUpdate(); setDismissed(true);
location.href = VERSION_URL;
} }
function handleDismissClick() { function handleDismissClick() {
updateCheck(); updateCheck();
forceUpdate(); setDismissed(true);
} }
if (!hasUpdate || checked) { useEffect(() => {
if (!checked) {
checkVersion();
}
}, []);
if (!hasUpdate || dismissed) {
return null; return null;
} }

View file

@ -3,11 +3,11 @@ import classNames from 'classnames';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link'; import Link from 'components/common/Link';
import styles from './Footer.module.css'; import styles from './Footer.module.css';
import useVersion from 'hooks/useVersion'; import useStore from 'store/version';
import { HOMEPAGE_URL, VERSION_URL } from 'lib/constants'; import { HOMEPAGE_URL, VERSION_URL } from 'lib/constants';
export default function Footer() { export default function Footer() {
const { current } = useVersion(); const { current } = useStore();
return ( return (
<footer className={classNames(styles.footer, 'row')}> <footer className={classNames(styles.footer, 'row')}>

View file

@ -19,7 +19,7 @@ export default function Header() {
return ( return (
<> <>
{user?.is_admin && <UpdateNotice />} {user?.is_admin && !process.env.updatesDisabled && <UpdateNotice />}
<header className={classNames(styles.header, 'row')}> <header className={classNames(styles.header, 'row')}>
<div className={styles.title}> <div className={styles.title}>
<Icon icon={<Logo />} size="large" className={styles.logo} /> <Icon icon={<Logo />} size="large" className={styles.logo} />

View file

@ -3,20 +3,16 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import Dot from 'components/common/Dot'; import Dot from 'components/common/Dot';
import { TOKEN_HEADER } from 'lib/constants';
import useShareToken from 'hooks/useShareToken';
import styles from './ActiveUsers.module.css'; import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) { export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
const shareToken = useShareToken();
const url = websiteId ? `/website/${websiteId}/active` : null; const url = websiteId ? `/website/${websiteId}/active` : null;
const { data } = useFetch(url, { const { data } = useFetch(url, {
interval, interval,
headers: { [TOKEN_HEADER]: shareToken?.token },
}); });
const count = useMemo(() => { const count = useMemo(() => {
if (websiteId) { if (websiteId) {
return data?.[0]?.x || 0 return data?.[0]?.x || 0;
} }
return value !== undefined ? value : 0; return value !== undefined ? value : 0;

View file

@ -1,14 +1,21 @@
import React from 'react'; import React from 'react';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
import FilterLink from 'components/common/FilterLink'; import FilterLink from 'components/common/FilterLink';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
const messages = defineMessages({
unknown: { id: 'label.unknown', defaultMessage: 'Unknown' },
countries: { id: 'metrics.countries', defaultMessage: 'Countries' },
visitors: { id: 'metrics.visitors', defaultMessage: 'Visitors' },
});
export default function CountriesTable({ websiteId, onDataLoad, ...props }) { export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
const { locale } = useLocale(); const { locale } = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
const { formatMessage } = useIntl();
function renderLink({ x: code }) { function renderLink({ x: code }) {
return ( return (
@ -16,9 +23,7 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
<FilterLink <FilterLink
id="country" id="country"
value={code} value={code}
label={ label={countryNames[code] ?? formatMessage(messages.unknown)}
countryNames[code] ?? <FormattedMessage id="label.unknown" defaultMessage="Unknown" />
}
/> />
</div> </div>
); );
@ -27,9 +32,9 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
return ( return (
<MetricsTable <MetricsTable
{...props} {...props}
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />} title={formatMessage(messages.countries)}
type="country" type="country"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />} metric={formatMessage(messages.visitors)}
websiteId={websiteId} websiteId={websiteId}
onDataLoad={data => onDataLoad?.(percentFilter(data))} onDataLoad={data => onDataLoad?.(percentFilter(data))}
renderLabel={renderLink} renderLabel={renderLink}

View file

@ -6,8 +6,7 @@ import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone'; import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import useShareToken from 'hooks/useShareToken'; import { EVENT_COLORS } from 'lib/constants';
import { EVENT_COLORS, TOKEN_HEADER } from 'lib/constants';
export default function EventsChart({ websiteId, className, token }) { export default function EventsChart({ websiteId, className, token }) {
const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId); const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
@ -15,7 +14,6 @@ export default function EventsChart({ websiteId, className, token }) {
const { const {
query: { url, eventType }, query: { url, eventType },
} = usePageQuery(); } = usePageQuery();
const shareToken = useShareToken();
const { data, loading } = useFetch( const { data, loading } = useFetch(
`/website/${websiteId}/events`, `/website/${websiteId}/events`,
@ -29,7 +27,6 @@ export default function EventsChart({ websiteId, className, token }) {
event_type: eventType, event_type: eventType,
token, token,
}, },
headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
[modified, eventType], [modified, eventType],
); );

View file

@ -6,14 +6,11 @@ import ErrorMessage from 'components/common/ErrorMessage';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import useShareToken from 'hooks/useShareToken';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format'; import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import { TOKEN_HEADER } from 'lib/constants';
import MetricCard from './MetricCard'; import MetricCard from './MetricCard';
import styles from './MetricsBar.module.css'; import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId, className }) { export default function MetricsBar({ websiteId, className }) {
const shareToken = useShareToken();
const [dateRange] = useDateRange(websiteId); const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange; const { startDate, endDate, modified } = dateRange;
const [format, setFormat] = useState(true); const [format, setFormat] = useState(true);
@ -34,7 +31,6 @@ export default function MetricsBar({ websiteId, className }) {
device, device,
country, country,
}, },
headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
[modified, url, referrer, os, browser, device, country], [modified, url, referrer, os, browser, device, country],
); );

View file

@ -9,10 +9,9 @@ import Arrow from 'assets/arrow-right.svg';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import useDateRange from 'hooks/useDateRange'; import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import useShareToken from 'hooks/useShareToken';
import ErrorMessage from 'components/common/ErrorMessage'; import ErrorMessage from 'components/common/ErrorMessage';
import DataTable from './DataTable'; import DataTable from './DataTable';
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants'; import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import styles from './MetricsTable.module.css'; import styles from './MetricsTable.module.css';
export default function MetricsTable({ export default function MetricsTable({
@ -25,7 +24,6 @@ export default function MetricsTable({
onDataLoad, onDataLoad,
...props ...props
}) { }) {
const shareToken = useShareToken();
const [{ startDate, endDate, modified }] = useDateRange(websiteId); const [{ startDate, endDate, modified }] = useDateRange(websiteId);
const { const {
resolve, resolve,
@ -49,7 +47,6 @@ export default function MetricsTable({
}, },
onDataLoad, onDataLoad,
delay: DEFAULT_ANIMATION_DURATION, delay: DEFAULT_ANIMATION_DURATION,
headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
[modified, url, referrer, os, browser, device, country], [modified, url, referrer, os, browser, device, country],
); );

View file

@ -12,9 +12,7 @@ import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone'; import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date'; import { getDateArray, getDateLength, getDateRangeValues } from 'lib/date';
import useShareToken from 'hooks/useShareToken';
import useApi from 'hooks/useApi'; import useApi from 'hooks/useApi';
import { TOKEN_HEADER } from 'lib/constants';
import styles from './WebsiteChart.module.css'; import styles from './WebsiteChart.module.css';
export default function WebsiteChart({ export default function WebsiteChart({
@ -26,7 +24,6 @@ export default function WebsiteChart({
showChart = true, showChart = true,
onDataLoad = () => {}, onDataLoad = () => {},
}) { }) {
const shareToken = useShareToken();
const [dateRange, setDateRange] = useDateRange(websiteId); const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange; const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone(); const [timezone] = useTimezone();
@ -53,7 +50,6 @@ export default function WebsiteChart({
country, country,
}, },
onDataLoad, onDataLoad,
headers: { [TOKEN_HEADER]: shareToken?.token },
}, },
[modified, url, referrer, os, browser, device, country], [modified, url, referrer, os, browser, device, country],
); );

View file

@ -32,7 +32,9 @@ export default function Dashboard() {
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<div>Dashboard</div> <div>
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</div>
<DashboardSettingsButton /> <DashboardSettingsButton />
</PageHeader> </PageHeader>
<WebsiteList websites={data} showCharts={showCharts} limit={max} /> <WebsiteList websites={data} showCharts={showCharts} limit={max} />

View file

@ -14,7 +14,7 @@ import useFetch from 'hooks/useFetch';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import { percentFilter } from 'lib/filters'; import { percentFilter } from 'lib/filters';
import { TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; import { SHARE_TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
import styles from './RealtimeDashboard.module.css'; import styles from './RealtimeDashboard.module.css';
function mergeData(state, data, time) { function mergeData(state, data, time) {
@ -38,7 +38,7 @@ export default function RealtimeDashboard() {
params: { start_at: data?.timestamp }, params: { start_at: data?.timestamp },
disabled: !init?.websites?.length || !data, disabled: !init?.websites?.length || !data,
interval: REALTIME_INTERVAL, interval: REALTIME_INTERVAL,
headers: { [TOKEN_HEADER]: init?.token }, headers: { [SHARE_TOKEN_HEADER]: init?.token },
}); });
const renderCountryName = useCallback( const renderCountryName = useCallback(

View file

@ -20,8 +20,7 @@ import EventsTable from 'components/metrics/EventsTable';
import EventsChart from 'components/metrics/EventsChart'; import EventsChart from 'components/metrics/EventsChart';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import useShareToken from 'hooks/useShareToken'; import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import { DEFAULT_ANIMATION_DURATION, TOKEN_HEADER } from 'lib/constants';
import styles from './WebsiteDetails.module.css'; import styles from './WebsiteDetails.module.css';
const views = { const views = {
@ -36,10 +35,7 @@ const views = {
}; };
export default function WebsiteDetails({ websiteId }) { export default function WebsiteDetails({ websiteId }) {
const shareToken = useShareToken(); const { data } = useFetch(`/website/${websiteId}`);
const { data } = useFetch(`/website/${websiteId}`, {
headers: { [TOKEN_HEADER]: shareToken?.token },
});
const [chartLoaded, setChartLoaded] = useState(false); const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState(); const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState(); const [eventsData, setEventsData] = useState();

View file

@ -97,3 +97,6 @@ ALTER TABLE `session` ADD FOREIGN KEY (`website_id`) REFERENCES `website`(`websi
-- AddForeignKey -- AddForeignKey
ALTER TABLE `website` ADD FOREIGN KEY (`user_id`) REFERENCES `account`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE `website` ADD FOREIGN KEY (`user_id`) REFERENCES `account`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- CreateAdminUser
INSERT INTO account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);

View file

@ -127,3 +127,6 @@ ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("websi
-- AddForeignKey -- AddForeignKey
ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- CreateAdminUser
INSERT INTO account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);

View file

@ -1,13 +1,18 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { get, post, put, del, getItem } from 'lib/web'; import { get, post, put, del, getItem } from 'lib/web';
import { AUTH_TOKEN } from 'lib/constants'; import { AUTH_TOKEN, SHARE_TOKEN_HEADER } from 'lib/constants';
import useStore from 'store/app';
function includeAuthToken(headers = {}) { const selector = state => state.shareToken;
const authToken = getItem(AUTH_TOKEN);
function parseHeaders(headers = {}, { authToken, shareToken }) {
if (authToken) { if (authToken) {
headers.Authorization = `Bearer ${authToken}`; headers.authorization = `Bearer ${authToken}`;
}
if (shareToken) {
headers[SHARE_TOKEN_HEADER] = shareToken.token;
} }
return headers; return headers;
@ -15,32 +20,50 @@ function includeAuthToken(headers = {}) {
export default function useApi() { export default function useApi() {
const { basePath } = useRouter(); const { basePath } = useRouter();
const authToken = getItem(AUTH_TOKEN);
const shareToken = useStore(selector);
return { return {
get: useCallback( get: useCallback(
async (url, params, headers) => { async (url, params, headers) => {
return get(`${basePath}/api${url}`, params, includeAuthToken(headers)); return get(
`${basePath}/api${url}`,
params,
parseHeaders(headers, { authToken, shareToken }),
);
}, },
[get], [get],
), ),
post: useCallback( post: useCallback(
async (url, params, headers) => { async (url, params, headers) => {
return post(`${basePath}/api${url}`, params, includeAuthToken(headers)); return post(
`${basePath}/api${url}`,
params,
parseHeaders(headers, { authToken, shareToken }),
);
}, },
[post], [post],
), ),
put: useCallback( put: useCallback(
async (url, params, headers) => { async (url, params, headers) => {
return put(`${basePath}/api${url}`, params, includeAuthToken(headers)); return put(
`${basePath}/api${url}`,
params,
parseHeaders(headers, { authToken, shareToken }),
);
}, },
[put], [put],
), ),
del: useCallback( del: useCallback(
async (url, params, headers) => { async (url, params, headers) => {
return del(`${basePath}/api${url}`, params, includeAuthToken(headers)); return del(
`${basePath}/api${url}`,
params,
parseHeaders(headers, { authToken, shareToken }),
);
}, },
[del], [del],
), ),

View file

@ -5,14 +5,14 @@ import useApi from './useApi';
export default function useFetch(url, options = {}, update = []) { export default function useFetch(url, options = {}, update = []) {
const [response, setResponse] = useState(); const [response, setResponse] = useState();
const [error, setError] = useState(); const [error, setError] = useState();
const [loading, setLoadiing] = useState(false); const [loading, setLoading] = useState(false);
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
const { get } = useApi(); const { get } = useApi();
const { params = {}, headers = {}, disabled, 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); setLoading(true);
setError(null); setError(null);
const time = performance.now(); const time = performance.now();
@ -32,7 +32,7 @@ export default function useFetch(url, options = {}, update = []) {
console.error(e); console.error(e);
setError(e); setError(e);
} finally { } finally {
setLoadiing(false); setLoading(false);
} }
} }

View file

@ -1,21 +0,0 @@
import { useEffect, useCallback } from 'react';
import useStore, { checkVersion } from 'store/version';
import { VERSION_CHECK } from 'lib/constants';
import { getItem, setItem } from 'lib/web';
export default function useVersion(check) {
const versions = useStore();
const checked = versions.latest === getItem(VERSION_CHECK)?.version;
const updateCheck = useCallback(() => {
setItem(VERSION_CHECK, { version: versions.latest, time: Date.now() });
}, [versions]);
useEffect(() => {
if (check && !versions.latest) {
checkVersion();
}
}, [versions, check]);
return { ...versions, checked, updateCheck };
}

View file

@ -4,6 +4,7 @@
"label.administrator", "label.administrator",
"label.name", "label.name",
"label.domain", "label.domain",
"label.theme",
"metrics.device.desktop", "metrics.device.desktop",
"metrics.device.laptop", "metrics.device.laptop",
"metrics.device.tablet", "metrics.device.tablet",

View file

@ -28,7 +28,7 @@
"label.enable-share-url": "Freigabe-URL aktivieren", "label.enable-share-url": "Freigabe-URL aktivieren",
"label.invalid": "Ungültig", "label.invalid": "Ungültig",
"label.invalid-domain": "Ungültige Domain", "label.invalid-domain": "Ungültige Domain",
"label.language": "Language", "label.language": "Sprache",
"label.last-days": "Letzten {x} Tage", "label.last-days": "Letzten {x} Tage",
"label.last-hours": "Letzten {x} Stunden", "label.last-hours": "Letzten {x} Stunden",
"label.logged-in-as": "Angemeldet als {username}", "label.logged-in-as": "Angemeldet als {username}",
@ -51,7 +51,7 @@
"label.settings": "Einstellungen", "label.settings": "Einstellungen",
"label.share-url": "Freigabe-URL", "label.share-url": "Freigabe-URL",
"label.single-day": "Ein Tag", "label.single-day": "Ein Tag",
"label.theme": "Theme", "label.theme": "Thema",
"label.this-month": "Diesen Monat", "label.this-month": "Diesen Monat",
"label.this-week": "Diese Woche", "label.this-week": "Diese Woche",
"label.this-year": "Dieses Jahr", "label.this-year": "Dieses Jahr",
@ -92,7 +92,7 @@
"metrics.countries": "Länder", "metrics.countries": "Länder",
"metrics.device.desktop": "Desktop", "metrics.device.desktop": "Desktop",
"metrics.device.laptop": "Laptop", "metrics.device.laptop": "Laptop",
"metrics.device.mobile": "Mobiltelefon", "metrics.device.mobile": "Handy",
"metrics.device.tablet": "Tablet", "metrics.device.tablet": "Tablet",
"metrics.devices": "Geräte", "metrics.devices": "Geräte",
"metrics.events": "Ereignisse", "metrics.events": "Ereignisse",

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": "انصراف",
@ -28,16 +28,16 @@
"label.enable-share-url": "فعال کردن اشتراک گذاری URL", "label.enable-share-url": "فعال کردن اشتراک گذاری URL",
"label.invalid": "نامعتبر", "label.invalid": "نامعتبر",
"label.invalid-domain": "دامنه‌ی نامعتبر", "label.invalid-domain": "دامنه‌ی نامعتبر",
"label.language": "Language", "label.language": "زبان",
"label.last-days": "لیست {x} روز", "label.last-days": "لیست {x} روز گذشته",
"label.last-hours": "لیست {x} ساعت", "label.last-hours": "لیست {x} ساعت گذشته",
"label.logged-in-as": "وارد شده به عنوان {username}", "label.logged-in-as": "وارد شده به عنوان {username}",
"label.login": "ورود", "label.login": "ورود",
"label.logout": "خروج", "label.logout": "خروج",
"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": "پروفایل",
@ -46,12 +46,12 @@
"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": "به اشتراک گذاری URL", "label.share-url": "به اشتراک گذاری URL",
"label.single-day": "یک روز", "label.single-day": "یک روز",
"label.theme": "Theme", "label.theme": "تم",
"label.this-month": "این ماه", "label.this-month": "این ماه",
"label.this-week": "این هفته", "label.this-week": "این هفته",
"label.this-year": "امسال", "label.this-year": "امسال",
@ -64,7 +64,7 @@
"label.websites": "وب‌سایت‌ها", "label.websites": "وب‌سایت‌ها",
"message.active-users": "{x} هم اکنون {x, plural, one {یک} other {از میان}}", "message.active-users": "{x} هم اکنون {x, plural, one {یک} other {از میان}}",
"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": "مشکلی پیش آمده است.",
@ -78,7 +78,7 @@
"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": "تمامی آمارهای این وب‌سایت حذف خواهد شد اما tracking code بدون تغییر باقی می‌ماند.",
"message.save-success": "با موفقیت ذخیره شد.", "message.save-success": "با موفقیت ذخیره شد.",
"message.share-url": "این URL به اشتراک گذاشته شده عمومی برای {target} است.", "message.share-url": "این URL به اشتراک گذاشته شده عمومی برای {target} است.",
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Toggle charts",
@ -99,7 +99,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

@ -5,7 +5,7 @@
"label.administrator": "Administrateur", "label.administrator": "Administrateur",
"label.all": "Tout", "label.all": "Tout",
"label.all-events": "Tous les événements", "label.all-events": "Tous les événements",
"label.all-time": "Toutes périodes", "label.all-time": "Toutes les données",
"label.all-websites": "Tous les sites web", "label.all-websites": "Tous les sites web",
"label.back": "Retour", "label.back": "Retour",
"label.cancel": "Annuler", "label.cancel": "Annuler",
@ -13,10 +13,10 @@
"label.confirm-password": "Confirmation du mot de passe", "label.confirm-password": "Confirmation du mot de passe",
"label.copy-to-clipboard": "Copier dans le presse papier", "label.copy-to-clipboard": "Copier dans le presse papier",
"label.current-password": "Mot de passe actuel", "label.current-password": "Mot de passe actuel",
"label.custom-range": "Intervalle personnalisé", "label.custom-range": "Période personnalisée",
"label.dashboard": "Tableau de bord", "label.dashboard": "Tableau de bord",
"label.date-range": "Intervalle", "label.date-range": "Période",
"label.default-date-range": "Intervalle par défaut", "label.default-date-range": "Période par défaut",
"label.delete": "Supprimer", "label.delete": "Supprimer",
"label.delete-account": "Supprimer le compte", "label.delete-account": "Supprimer le compte",
"label.delete-website": "Supprimer le site", "label.delete-website": "Supprimer le site",
@ -25,7 +25,7 @@
"label.edit": "Modifier", "label.edit": "Modifier",
"label.edit-account": "Modifier le compte", "label.edit-account": "Modifier le compte",
"label.edit-website": "Modifier le site", "label.edit-website": "Modifier le site",
"label.enable-share-url": "Activer le partage d'URL", "label.enable-share-url": "Activer l'URL de partage",
"label.invalid": "Invalide", "label.invalid": "Invalide",
"label.invalid-domain": "Domaine invalide", "label.invalid-domain": "Domaine invalide",
"label.language": "Langage", "label.language": "Langage",
@ -68,10 +68,10 @@
"message.copied": "Copié !", "message.copied": "Copié !",
"message.delete-warning": "Toutes les données associées seront également supprimées.", "message.delete-warning": "Toutes les données associées seront également supprimées.",
"message.failure": "Un problème est survenu.", "message.failure": "Un problème est survenu.",
"message.get-share-url": "Obtenez l'URL de partage", "message.get-share-url": "Obtenir l'URL de partage",
"message.get-tracking-code": "Obtenez le code de suivi", "message.get-tracking-code": "Obtenir le code de suivi",
"message.go-to-settings": "Aller aux paramètres", "message.go-to-settings": "Aller aux paramètres",
"message.incorrect-username-password": "nom d'utilisateurs/mot de passe incorrect.", "message.incorrect-username-password": "Nom d'utilisateur/Mot de passe incorrect.",
"message.log.visitor": "Visiteur de {country} utilisant {browser} sur {os} {device}", "message.log.visitor": "Visiteur de {country} utilisant {browser} sur {os} {device}",
"message.new-version-available": "Une nouvelle version de umami {version} est disponible !", "message.new-version-available": "Une nouvelle version de umami {version} est disponible !",
"message.no-data-available": "Pas de données disponibles.", "message.no-data-available": "Pas de données disponibles.",
@ -81,7 +81,7 @@
"message.reset-warning": "Toutes les statistiques pour ce site seront supprimés, mais votre code de suivi restera intact.", "message.reset-warning": "Toutes les statistiques pour ce site seront supprimés, mais votre code de suivi restera intact.",
"message.save-success": "Enregistré avec succès.", "message.save-success": "Enregistré avec succès.",
"message.share-url": "Ceci est l'URL partagée pour {target}.", "message.share-url": "Ceci est l'URL partagée pour {target}.",
"message.toggle-charts": "Changer les graphiques", "message.toggle-charts": "Afficher/Masquer les graphiques",
"message.track-stats": "Pour suivre les statistiques de {target}, placez le code suivant dans la section {head} de votre site Web.", "message.track-stats": "Pour suivre les statistiques de {target}, placez le code suivant dans la section {head} de votre site Web.",
"message.type-delete": "Tapez {delete} dans la case ci-dessous pour confirmer.", "message.type-delete": "Tapez {delete} dans la case ci-dessous pour confirmer.",
"message.type-reset": "Tapez {reset} dans la case ci-dessous pour confirmer.", "message.type-reset": "Tapez {reset} dans la case ci-dessous pour confirmer.",
@ -98,12 +98,12 @@
"metrics.events": "Événements", "metrics.events": "Événements",
"metrics.filter.combined": "Combiné", "metrics.filter.combined": "Combiné",
"metrics.filter.domain-only": "Domaine uniquement", "metrics.filter.domain-only": "Domaine uniquement",
"metrics.filter.raw": "Brute", "metrics.filter.raw": "Brut",
"metrics.languages": "Langages", "metrics.languages": "Langages",
"metrics.operating-systems": "Systèmes d'exploitation", "metrics.operating-systems": "Systèmes d'exploitation",
"metrics.page-views": "Pages vues", "metrics.page-views": "Pages vues",
"metrics.pages": "Pages", "metrics.pages": "Pages",
"metrics.referrers": "URL Référentes", "metrics.referrers": "Sources",
"metrics.unique-visitors": "Visiteurs uniques", "metrics.unique-visitors": "Visiteurs uniques",
"metrics.views": "Vues", "metrics.views": "Vues",
"metrics.visitors": "Visiteurs" "metrics.visitors": "Visiteurs"

110
lang/ga-ES.json Normal file
View file

@ -0,0 +1,110 @@
{
"label.accounts": "Contas",
"label.add-account": "Engadir conta",
"label.add-website": "Engadir sitio web",
"label.administrator": "Administradora",
"label.all": "Todo",
"label.all-events": "Tódolos eventos",
"label.all-time": "Sempre",
"label.all-websites": "Tódolos sitios web",
"label.back": "Atrás",
"label.cancel": "Cancelar",
"label.change-password": "Mudar contrasinal",
"label.confirm-password": "Confirmar contrasinal",
"label.copy-to-clipboard": "Copiar ao portapapeis",
"label.current-password": "Contrasinal actual",
"label.custom-range": "Rango personalizado",
"label.dashboard": "Taboleiro",
"label.date-range": "Rango temporal",
"label.default-date-range": "Rango temporal por defecto",
"label.delete": "Eliminar",
"label.delete-account": "Eliminar conta",
"label.delete-website": "Eliminar sitio web",
"label.dismiss": "Desbotar",
"label.domain": "Dominio",
"label.edit": "Editar",
"label.edit-account": "Editar conta",
"label.edit-website": "Editar sitio web",
"label.enable-share-url": "Activar URL de compartición",
"label.invalid": "Non válido",
"label.invalid-domain": "Dominio non válido",
"label.language": "Idioma",
"label.last-days": "Últimos {x} días",
"label.last-hours": "Últimas {x} horas",
"label.logged-in-as": "Sesión de {username}",
"label.login": "Acceder",
"label.logout": "Pechar sesión",
"label.more": "Máis",
"label.name": "Nome",
"label.new-password": "Novo contrasinal",
"label.owner": "Dona",
"label.password": "Contrasinal",
"label.passwords-dont-match": "Non concordan os contrasinais",
"label.profile": "Perfil",
"label.realtime": "Agora mesmo",
"label.realtime-logs": "Rexistro neste intre",
"label.refresh": "Actualizar",
"label.required": "Requerido",
"label.reset": "Restablecer",
"label.reset-website": "Restablecer estatísticas",
"label.save": "Gardar",
"label.settings": "Axustes",
"label.share-url": "Compartir URL",
"label.single-day": "Un só día",
"label.theme": "Decorado",
"label.this-month": "Este mes",
"label.this-week": "Esta semana",
"label.this-year": "Este ano",
"label.timezone": "Zona horaria",
"label.today": "Hoxe",
"label.tracking-code": "Código de seguimento",
"label.unknown": "Descoñecido",
"label.username": "Identificador",
"label.view-details": "Ver detalles",
"label.websites": "Sitios web",
"message.active-users": "{x} actual {x, plural, one {visitante} other {visitantes}}",
"message.confirm-delete": "Tes a certeza de querer eliminar {target}?",
"message.confirm-reset": "Tes a certeza de querer restablecer as estatísticas de {target}?",
"message.copied": "Copiado!",
"message.delete-warning": "Tamén serán borrados tódolos datos asociados.",
"message.failure": "Houbo un fallo.",
"message.get-share-url": "Obter URL de compartición",
"message.get-tracking-code": "Obter código de seguimento",
"message.go-to-settings": "Ir aos axustes",
"message.incorrect-username-password": "Credenciais incorrectas.",
"message.log.visitor": "Visitante desde {country} usando {browser} en {os} {device}",
"message.new-version-available": "A nova versión {version} de umami está dispoñible!",
"message.no-data-available": "Sen datos dispoñibles.",
"message.no-websites-configured": "Non tes sitios web configurados.",
"message.page-not-found": "Páxina non atopada.",
"message.powered-by": "Funciona grazas a {name}",
"message.reset-warning": "Vanse eliminar tódalas estatísticas deste sitio web, pero o código de seguimento permanecerá sen cambios.",
"message.save-success": "Gardouse correctamente.",
"message.share-url": "Este é o URL da compartición pública de {target}.",
"message.toggle-charts": "Activación das gráficas",
"message.track-stats": "Para crear estatísticas de {target}, pon este código na sección {head} do teu sitio web.",
"message.type-delete": "Escribe {delete} na caixa inferior para confirmar.",
"message.type-reset": "Escribe {reset} na caixa inferior para confirmar.",
"metrics.actions": "Accións",
"metrics.average-visit-time": "Tempo medio de visita",
"metrics.bounce-rate": "Proporción de rebote",
"metrics.browsers": "Navegadores",
"metrics.countries": "Países",
"metrics.device.desktop": "Escritorio",
"metrics.device.laptop": "Portátil",
"metrics.device.mobile": "Móbil",
"metrics.device.tablet": "Tableta",
"metrics.devices": "Dispositivos",
"metrics.events": "Eventos",
"metrics.filter.combined": "Combinado",
"metrics.filter.domain-only": "Só dominio",
"metrics.filter.raw": "Raw",
"metrics.languages": "Idiomas",
"metrics.operating-systems": "Sistemas operativos",
"metrics.page-views": "Vistas de páxinas",
"metrics.pages": "Páxinas",
"metrics.referrers": "Orixes",
"metrics.unique-visitors": "Visitas únicas",
"metrics.views": "Visualizacións",
"metrics.visitors": "Visitantes"
}

View file

@ -12,7 +12,7 @@
"label.change-password": "Modifica password", "label.change-password": "Modifica password",
"label.confirm-password": "Conferma password", "label.confirm-password": "Conferma password",
"label.copy-to-clipboard": "Copia", "label.copy-to-clipboard": "Copia",
"label.current-password": "Password corrente", "label.current-password": "Password attuale",
"label.custom-range": "Personalizzato", "label.custom-range": "Personalizzato",
"label.dashboard": "Pannello di Controllo", "label.dashboard": "Pannello di Controllo",
"label.date-range": "Periodo", "label.date-range": "Periodo",
@ -28,7 +28,7 @@
"label.enable-share-url": "Abilita URL di condivisione", "label.enable-share-url": "Abilita URL di condivisione",
"label.invalid": "Non valido", "label.invalid": "Non valido",
"label.invalid-domain": "Dominio non valido", "label.invalid-domain": "Dominio non valido",
"label.language": "Language", "label.language": "Lingua",
"label.last-days": "Ultimi {x} giorni", "label.last-days": "Ultimi {x} giorni",
"label.last-hours": "Ultime {x} ore", "label.last-hours": "Ultime {x} ore",
"label.logged-in-as": "Ciao {username}", "label.logged-in-as": "Ciao {username}",
@ -51,7 +51,7 @@
"label.settings": "Impostazioni", "label.settings": "Impostazioni",
"label.share-url": "Condividi link", "label.share-url": "Condividi link",
"label.single-day": "Singolo giorno", "label.single-day": "Singolo giorno",
"label.theme": "Theme", "label.theme": "Tema",
"label.this-month": "Questo mese", "label.this-month": "Questo mese",
"label.this-week": "Questa settimana", "label.this-week": "Questa settimana",
"label.this-year": "Quest'anno", "label.this-year": "Quest'anno",
@ -64,7 +64,7 @@
"label.websites": "Siti web", "label.websites": "Siti web",
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online", "message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
"message.confirm-delete": "Sei sicuro di voler eliminare {target}?", "message.confirm-delete": "Sei sicuro di voler eliminare {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", "message.confirm-reset": "Sei sicuro di voler azzerare le statistiche di {target}?",
"message.copied": "Copiato!", "message.copied": "Copiato!",
"message.delete-warning": "Saranno eliminati anche tutti i dati associati.", "message.delete-warning": "Saranno eliminati anche tutti i dati associati.",
"message.failure": "Si è verificato un errore.", "message.failure": "Si è verificato un errore.",
@ -73,7 +73,7 @@
"message.go-to-settings": "Vai alle impostazioni", "message.go-to-settings": "Vai alle impostazioni",
"message.incorrect-username-password": "Username o password non corretti.", "message.incorrect-username-password": "Username o password non corretti.",
"message.log.visitor": "Utenti da {country} tramite {browser} su {os} {device}", "message.log.visitor": "Utenti da {country} tramite {browser} su {os} {device}",
"message.new-version-available": "Una nuova versione umami {version} è disponibile!", "message.new-version-available": "Una nuova versione {version} di umami è disponibile!",
"message.no-data-available": "Nessun dato disponibile.", "message.no-data-available": "Nessun dato disponibile.",
"message.no-websites-configured": "Non hai ancora configurato alcun sito.", "message.no-websites-configured": "Non hai ancora configurato alcun sito.",
"message.page-not-found": "Pagina non trovata", "message.page-not-found": "Pagina non trovata",
@ -103,7 +103,7 @@
"metrics.operating-systems": "Sistemi operativi", "metrics.operating-systems": "Sistemi operativi",
"metrics.page-views": "Visualizzazioni di pagina", "metrics.page-views": "Visualizzazioni di pagina",
"metrics.pages": "Pagine", "metrics.pages": "Pagine",
"metrics.referrers": "Referr", "metrics.referrers": "Referrers",
"metrics.unique-visitors": "Visitatori unici", "metrics.unique-visitors": "Visitatori unici",
"metrics.views": "Visualizzazioni", "metrics.views": "Visualizzazioni",
"metrics.visitors": "Visitatori" "metrics.visitors": "Visitatori"

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": "Цуцлах",
@ -28,7 +28,7 @@
"label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх", "label.enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",
"label.invalid": "Буруу", "label.invalid": "Буруу",
"label.invalid-domain": "Буруу домэйн", "label.invalid-domain": "Буруу домэйн",
"label.language": "Language", "label.language": "Хэл",
"label.last-days": "Сүүлийн {x} хоног", "label.last-days": "Сүүлийн {x} хоног",
"label.last-hours": "Сүүлийн {x} цаг", "label.last-hours": "Сүүлийн {x} цаг",
"label.logged-in-as": "{username}-р нэвтэрсэн", "label.logged-in-as": "{username}-р нэвтэрсэн",
@ -37,7 +37,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": "Бүртгэл",
@ -46,12 +46,12 @@
"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": "Хуваалцах холбоос",
"label.single-day": "Нэг өдөр", "label.single-day": "Нэг өдөр",
"label.theme": "Theme", "label.theme": "Загвар",
"label.this-month": "Энэ сар", "label.this-month": "Энэ сар",
"label.this-week": "Энэ долоо хоног", "label.this-week": "Энэ долоо хоног",
"label.this-year": "Энэ жил", "label.this-year": "Энэ жил",
@ -78,10 +78,10 @@
"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": "Доорх хэсэгт {reset} гэж бичиж баталгаажуулна уу.", "message.type-reset": "Доорх хэсэгт {reset} гэж бичиж баталгаажуулна уу.",
@ -99,7 +99,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

@ -4,8 +4,8 @@
"label.add-website": "Adicionar site", "label.add-website": "Adicionar site",
"label.administrator": "Administrador", "label.administrator": "Administrador",
"label.all": "Todos", "label.all": "Todos",
"label.all-events": "All events", "label.all-events": "Todos os eventos",
"label.all-time": "All time", "label.all-time": "Todo o período",
"label.all-websites": "Todos os sites", "label.all-websites": "Todos os sites",
"label.back": "Voltar", "label.back": "Voltar",
"label.cancel": "Cancelar", "label.cancel": "Cancelar",
@ -28,7 +28,7 @@
"label.enable-share-url": "Ativar link de compartilhamento", "label.enable-share-url": "Ativar link de compartilhamento",
"label.invalid": "Inválido", "label.invalid": "Inválido",
"label.invalid-domain": "Domínio inválido", "label.invalid-domain": "Domínio inválido",
"label.language": "Language", "label.language": "Idioma",
"label.last-days": "Últimos {x} dias", "label.last-days": "Últimos {x} dias",
"label.last-hours": "Últimas {x} horas", "label.last-hours": "Últimas {x} horas",
"label.logged-in-as": "Sessão iniciada como {username}", "label.logged-in-as": "Sessão iniciada como {username}",
@ -37,7 +37,7 @@
"label.more": "Mais", "label.more": "Mais",
"label.name": "Nome", "label.name": "Nome",
"label.new-password": "Nova senha", "label.new-password": "Nova senha",
"label.owner": "Owner", "label.owner": "Proprietário",
"label.password": "Senha", "label.password": "Senha",
"label.passwords-dont-match": "As senhas não correspondem", "label.passwords-dont-match": "As senhas não correspondem",
"label.profile": "Perfil", "label.profile": "Perfil",
@ -46,12 +46,12 @@
"label.refresh": "Atualizar", "label.refresh": "Atualizar",
"label.required": "Obrigatório", "label.required": "Obrigatório",
"label.reset": "Redefinir", "label.reset": "Redefinir",
"label.reset-website": "Reset statistics", "label.reset-website": "Redefinir estatísticas",
"label.save": "Salvar", "label.save": "Salvar",
"label.settings": "Configurações", "label.settings": "Configurações",
"label.share-url": "Link de compartilhamento", "label.share-url": "Link de compartilhamento",
"label.single-day": "Dia específico", "label.single-day": "Dia específico",
"label.theme": "Theme", "label.theme": "Tema",
"label.this-month": "Este mês", "label.this-month": "Este mês",
"label.this-week": "Esta semana", "label.this-week": "Esta semana",
"label.this-year": "Este ano", "label.this-year": "Este ano",
@ -64,7 +64,7 @@
"label.websites": "Sites", "label.websites": "Sites",
"message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento", "message.active-users": "{x} {x, plural, one {visitante} other {visitantes}} neste momento",
"message.confirm-delete": "Deseja realmente remover {target}?", "message.confirm-delete": "Deseja realmente remover {target}?",
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?", "message.confirm-reset": "Você tem certeza que deseja redefinir as estatísticas de {target}?",
"message.copied": "Copiado!", "message.copied": "Copiado!",
"message.delete-warning": "Todos os dados associados também serão eliminados.", "message.delete-warning": "Todos os dados associados também serão eliminados.",
"message.failure": "Ocorreu um erro.", "message.failure": "Ocorreu um erro.",
@ -78,10 +78,10 @@
"message.no-websites-configured": "Nenhum site foi configurado ainda.", "message.no-websites-configured": "Nenhum site foi configurado ainda.",
"message.page-not-found": "Página não encontrada.", "message.page-not-found": "Página não encontrada.",
"message.powered-by": "Distribuído por {name}", "message.powered-by": "Distribuído por {name}",
"message.reset-warning": "All statistics for this website will be deleted, but your tracking code will remain intact.", "message.reset-warning": "Todas as estatísticas deste site serão removidas, mas seu código de rastreamento permanecerá intacto.",
"message.save-success": "Salvo com sucesso.", "message.save-success": "Salvo com sucesso.",
"message.share-url": "Este é o link público de compartilhamento para {target}.", "message.share-url": "Este é o link público de compartilhamento para {target}.",
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Mostrar/Esconder gráficos",
"message.track-stats": "Para gerar estatística para {target}, coloque o seguinte código no {head} do html do seu site.", "message.track-stats": "Para gerar estatística para {target}, coloque o seguinte código no {head} do html do seu site.",
"message.type-delete": "Escreva {delete} abaixo para continuar.", "message.type-delete": "Escreva {delete} abaixo para continuar.",
"message.type-reset": "Escreva {reset} abaixo para continuar.", "message.type-reset": "Escreva {reset} abaixo para continuar.",
@ -99,7 +99,7 @@
"metrics.filter.combined": "Combinado", "metrics.filter.combined": "Combinado",
"metrics.filter.domain-only": "Apenas domínio", "metrics.filter.domain-only": "Apenas domínio",
"metrics.filter.raw": "Dados brutos", "metrics.filter.raw": "Dados brutos",
"metrics.languages": "Languages", "metrics.languages": "Idiomas",
"metrics.operating-systems": "Sistemas operacionais", "metrics.operating-systems": "Sistemas operacionais",
"metrics.page-views": "Visualizações de página", "metrics.page-views": "Visualizações de página",
"metrics.pages": "Páginas", "metrics.pages": "Páginas",

View file

@ -28,7 +28,7 @@
"label.enable-share-url": "Разрешить делиться ссылкой", "label.enable-share-url": "Разрешить делиться ссылкой",
"label.invalid": "Некорректный", "label.invalid": "Некорректный",
"label.invalid-domain": "Некорректный домен", "label.invalid-domain": "Некорректный домен",
"label.language": "Language", "label.language": "Язык",
"label.last-days": "Последние {x} дней", "label.last-days": "Последние {x} дней",
"label.last-hours": "Последние {x} часа", "label.last-hours": "Последние {x} часа",
"label.logged-in-as": "Вы вошли как {username}", "label.logged-in-as": "Вы вошли как {username}",
@ -51,7 +51,7 @@
"label.settings": "Настройки", "label.settings": "Настройки",
"label.share-url": "Поделиться ссылкой", "label.share-url": "Поделиться ссылкой",
"label.single-day": "Один день", "label.single-day": "Один день",
"label.theme": "Theme", "label.theme": "Тема",
"label.this-month": "Этот месяц", "label.this-month": "Этот месяц",
"label.this-week": "Эта неделя", "label.this-week": "Эта неделя",
"label.this-year": "Этот год", "label.this-year": "Этот год",

View file

@ -2,11 +2,11 @@
"label.accounts": "Tài khoản", "label.accounts": "Tài khoản",
"label.add-account": "Thêm tài khoản", "label.add-account": "Thêm tài khoản",
"label.add-website": "Thêm website", "label.add-website": "Thêm website",
"label.administrator": "Quản Trị", "label.administrator": "Quản trị",
"label.all": "Tất cả", "label.all": "Tất cả",
"label.all-events": "Tất cả events", "label.all-events": "Tất cả sự kiện",
"label.all-time": "All time", "label.all-time": "Toàn thời gian",
"label.all-websites": "Tất cả websites", "label.all-websites": "Tất cả website",
"label.back": "Quay về", "label.back": "Quay về",
"label.cancel": "Huỷ bỏ", "label.cancel": "Huỷ bỏ",
"label.change-password": "Đổi mật khẩu", "label.change-password": "Đổi mật khẩu",
@ -16,7 +16,7 @@
"label.custom-range": "Phạm vi ngày tuỳ chọn", "label.custom-range": "Phạm vi ngày tuỳ chọn",
"label.dashboard": "Bảng điều khiển", "label.dashboard": "Bảng điều khiển",
"label.date-range": "Phạm vi ngày", "label.date-range": "Phạm vi ngày",
"label.default-date-range": "Phạm vi ngày mặc định", "label.default-date-range": "Khoảng thời gian mặc định",
"label.delete": "Xoá", "label.delete": "Xoá",
"label.delete-account": "Xoá tài khoản", "label.delete-account": "Xoá tài khoản",
"label.delete-website": "Xáo website", "label.delete-website": "Xáo website",
@ -37,7 +37,7 @@
"label.more": "Thêm", "label.more": "Thêm",
"label.name": "Tên", "label.name": "Tên",
"label.new-password": "Mật khẩu mới", "label.new-password": "Mật khẩu mới",
"label.owner": "Owner", "label.owner": "Chủ nhân",
"label.password": "Mật khẩu", "label.password": "Mật khẩu",
"label.passwords-dont-match": "Mật khẩu không đồng nhất", "label.passwords-dont-match": "Mật khẩu không đồng nhất",
"label.profile": "Hồ sơ", "label.profile": "Hồ sơ",
@ -81,7 +81,7 @@
"message.reset-warning": "Tất cả số liệu thống kê của website này sẽ bị xoá, nhưng mã theo dõi sẽ vẫn giữ nguyên.", "message.reset-warning": "Tất cả số liệu thống kê của website này sẽ bị xoá, nhưng mã theo dõi sẽ vẫn giữ nguyên.",
"message.save-success": "Đã lưu thành công.", "message.save-success": "Đã lưu thành công.",
"message.share-url": "Đây là đường dẫn URL cho {target}.", "message.share-url": "Đây là đường dẫn URL cho {target}.",
"message.toggle-charts": "Toggle charts", "message.toggle-charts": "Bật/tắt biểu đồ",
"message.track-stats": "Để theo dõi {target}, dán mã theo dõi vào {head} của website bạn.", "message.track-stats": "Để theo dõi {target}, dán mã theo dõi vào {head} của website bạn.",
"message.type-delete": "Nhập {delete} bên dưới để xác nhận.", "message.type-delete": "Nhập {delete} bên dưới để xác nhận.",
"message.type-reset": "Nhập {reset} bên dưới để xác nhận.", "message.type-reset": "Nhập {reset} bên dưới để xác nhận.",
@ -99,12 +99,12 @@
"metrics.filter.combined": "Kết hợp", "metrics.filter.combined": "Kết hợp",
"metrics.filter.domain-only": "Chỉ tên miền", "metrics.filter.domain-only": "Chỉ tên miền",
"metrics.filter.raw": "Gốc", "metrics.filter.raw": "Gốc",
"metrics.languages": "Languages", "metrics.languages": "Ngôn ngũ",
"metrics.operating-systems": "Hệ điều hành", "metrics.operating-systems": "Hệ điều hành",
"metrics.page-views": "Lượt xem", "metrics.page-views": "Lượt xem",
"metrics.pages": "Trang", "metrics.pages": "Trang",
"metrics.referrers": "Liên kết giới thiệu", "metrics.referrers": "Liên kết giới thiệu",
"metrics.unique-visitors": "Khách truy cập duy nhất", "metrics.unique-visitors": "Khách truy cập một lần",
"metrics.views": "Xem", "metrics.views": "Xem",
"metrics.visitors": "Khách" "metrics.visitors": "Khách"
} }

View file

@ -1,5 +1,5 @@
import { parseSecureToken, parseToken } from './crypto'; import { parseSecureToken, parseToken } from './crypto';
import { TOKEN_HEADER } from './constants'; import { SHARE_TOKEN_HEADER } from './constants';
import { getWebsiteById } from './queries'; import { getWebsiteById } from './queries';
export async function getAuthToken(req) { export async function getAuthToken(req) {
@ -30,7 +30,7 @@ export async function isValidToken(token, validation) {
export async function allowQuery(req, skipToken) { export async function allowQuery(req, skipToken) {
const { id } = req.query; const { id } = req.query;
const token = req.headers[TOKEN_HEADER]; const token = req.headers[SHARE_TOKEN_HEADER];
const websiteId = +id; const websiteId = +id;
const website = await getWebsiteById(websiteId); const website = await getWebsiteById(websiteId);

View file

@ -5,7 +5,7 @@ export const DATE_RANGE_CONFIG = 'umami.date-range';
export const THEME_CONFIG = 'umami.theme'; export const THEME_CONFIG = 'umami.theme';
export const DASHBOARD_CONFIG = 'umami.dashboard'; export const DASHBOARD_CONFIG = 'umami.dashboard';
export const VERSION_CHECK = 'umami.version-check'; export const VERSION_CHECK = 'umami.version-check';
export const TOKEN_HEADER = 'x-umami-token'; export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
export const HOMEPAGE_URL = 'https://umami.is'; export const HOMEPAGE_URL = 'https://umami.is';
export const VERSION_URL = 'https://github.com/mikecao/umami/releases'; export const VERSION_URL = 'https://github.com/mikecao/umami/releases';

View file

@ -14,11 +14,11 @@ export function hash(...args) {
} }
export function secret() { export function secret() {
return hash(process.env.HASH_SALT); return hash(process.env.HASH_SALT || process.env.DATABASE_URL);
} }
export function salt() { export function salt() {
return v5([secret(), ROTATING_SALT].join(''), v5.DNS); return v5(hash(secret(), ROTATING_SALT), v5.DNS);
} }
export function uuid(...args) { export function uuid(...args) {

View file

@ -11,23 +11,23 @@ const options = {
}; };
function logQuery(e) { function logQuery(e) {
if (process.env.LOG_QUERY) {
console.log(chalk.yellow(e.params), '->', e.query, chalk.greenBright(`${e.duration}ms`)); console.log(chalk.yellow(e.params), '->', e.query, chalk.greenBright(`${e.duration}ms`));
}
} }
let prisma; let prisma;
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient(options); prisma = new PrismaClient(options);
prisma.$on('query', logQuery);
} else { } else {
if (!global.prisma) { if (!global.prisma) {
global.prisma = new PrismaClient(options); global.prisma = new PrismaClient(options);
global.prisma.$on('query', logQuery);
} }
prisma = global.prisma; prisma = global.prisma;
} }
if (process.env.LOG_QUERY) {
prisma.$on('query', logQuery);
}
export default prisma; export default prisma;

View file

@ -53,6 +53,7 @@ export const languages = {
'fa-IR': { label: 'فارسی', dateLocale: faIR, dir: 'rtl' }, 'fa-IR': { label: 'فارسی', dateLocale: faIR, dir: 'rtl' },
'fo-FO': { label: 'Føroyskt' }, 'fo-FO': { label: 'Føroyskt' },
'fr-FR': { label: 'Français', dateLocale: fr }, 'fr-FR': { label: 'Français', dateLocale: fr },
'ga-ES': { label: 'Galacian (Spain)', dateLocale: es },
'el-GR': { label: 'Ελληνικά', dateLocale: el }, 'el-GR': { label: 'Ελληνικά', dateLocale: el },
'he-IL': { label: 'עברית', dateLocale: he }, 'he-IL': { label: 'עברית', dateLocale: he },
'hi-IN': { label: 'हिन्दी', dateLocale: hi }, 'hi-IN': { label: 'हिन्दी', dateLocale: hi },

View file

@ -64,7 +64,7 @@ export function getFilterQuery(table, filters = {}, params = []) {
switch (key) { switch (key) {
case 'url': case 'url':
if (table === 'session' || table === 'pageview') { if (table === 'pageview' || table === 'event') {
arr.push(`and ${table}.${key}=$${params.length + 1}`); arr.push(`and ${table}.${key}=$${params.length + 1}`);
params.push(decodeURIComponent(value)); params.push(decodeURIComponent(value));
} }
@ -110,11 +110,11 @@ export function getFilterQuery(table, filters = {}, params = []) {
} }
export function parseFilters(table, filters = {}, params = []) { export function parseFilters(table, filters = {}, params = []) {
const { domain, url, referrer, os, browser, device, country, event_type } = filters; const { domain, url, event_url, referrer, os, browser, device, country, event_type } = filters;
const pageviewFilters = { domain, url, referrer }; const pageviewFilters = { domain, url, referrer };
const sessionFilters = { os, browser, device, country }; const sessionFilters = { os, browser, device, country };
const eventFilters = { event_type }; const eventFilters = { url: event_url, event_type };
return { return {
pageviewFilters, pageviewFilters,
@ -315,6 +315,13 @@ export async function getAccounts() {
username: 'asc', username: 'asc',
}, },
], ],
select: {
user_id: true,
username: true,
is_admin: true,
created_at: true,
updated_at: true,
},
}), }),
); );
} }
@ -502,8 +509,11 @@ export function getSessionMetrics(website_id, start_at, end_at, field, filters =
export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) { export function getPageviewMetrics(website_id, start_at, end_at, field, table, filters = {}) {
const params = [website_id, start_at, end_at]; const params = [website_id, start_at, end_at];
console.log({ table, filters }); const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters(
const { pageviewQuery, sessionQuery, joinSession } = parseFilters(table, filters, params); table,
filters,
params,
);
return rawQuery( return rawQuery(
` `
@ -514,6 +524,7 @@ export function getPageviewMetrics(website_id, start_at, end_at, field, table, f
and ${table}.created_at between $2 and $3 and ${table}.created_at between $2 and $3
${pageviewQuery} ${pageviewQuery}
${joinSession && sessionQuery} ${joinSession && sessionQuery}
${eventQuery}
group by 1 group by 1
order by 2 desc order by 2 desc
`, `,

View file

@ -3,9 +3,14 @@ const pkg = require('./package.json');
module.exports = { module.exports = {
env: { env: {
VERSION: pkg.version, currentVersion: pkg.version,
loginDisabled: process.env.DISABLE_LOGIN,
updatesDisabled: process.env.DISABLE_UPDATES,
}, },
basePath: process.env.BASE_PATH, basePath: process.env.BASE_PATH,
experimental: {
outputStandalone: true,
},
eslint: { eslint: {
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "1.30.0", "version": "1.33.0",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.", "description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
@ -12,20 +12,21 @@
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "npm-run-all build-tracker build-geo build-db build-app", "build": "npm-run-all build-tracker build-geo build-db build-app",
"start": "next start", "start": "npm-run-all check-db start-next",
"start-docker": "npm-run-all check-db start-server",
"start-env": "node -r dotenv/config scripts/start-env.js", "start-env": "node -r dotenv/config scripts/start-env.js",
"start-server": "node server.js",
"start-next": "next start",
"build-app": "next build", "build-app": "next build",
"build-tracker": "rollup -c rollup.tracker.config.js", "build-tracker": "rollup -c rollup.tracker.config.js",
"build-db": "npm-run-all copy-db-schema build-db-client", "build-db": "npm-run-all copy-db-files build-db-client",
"build-lang": "npm-run-all format-lang compile-lang", "build-lang": "npm-run-all format-lang compile-lang",
"build-geo": "node scripts/build-geo.js", "build-geo": "node scripts/build-geo.js",
"build-db-schema": "dotenv prisma introspect", "build-db-schema": "prisma db pull",
"build-db-client": "dotenv prisma generate", "build-db-client": "prisma generate",
"build-mysql-schema": "dotenv prisma db pull -- --schema=./prisma/schema.mysql.prisma", "update-db": "prisma migrate deploy",
"build-mysql-client": "dotenv prisma generate -- --schema=./prisma/schema.mysql.prisma", "check-db": "node scripts/check-db.js",
"build-postgresql-schema": "dotenv prisma db pull -- --schema=./prisma/schema.postgresql.prisma", "copy-db-files": "node scripts/copy-db-files.js",
"build-postgresql-client": "dotenv prisma generate -- --schema=./prisma/schema.postgresql.prisma",
"copy-db-schema": "node scripts/copy-db-schema.js",
"generate-lang": "npm-run-all extract-lang merge-lang", "generate-lang": "npm-run-all extract-lang merge-lang",
"extract-lang": "formatjs extract \"{pages,components}/**/*.js\" --out-file build/messages.json", "extract-lang": "formatjs extract \"{pages,components}/**/*.js\" --out-file build/messages.json",
"merge-lang": "node scripts/merge-lang.js", "merge-lang": "node scripts/merge-lang.js",
@ -36,7 +37,7 @@
"download-language-names": "node scripts/download-language-names.js", "download-language-names": "node scripts/download-language-names.js",
"change-password": "node scripts/change-password.js", "change-password": "node scripts/change-password.js",
"lint": "next lint --quiet", "lint": "next lint --quiet",
"prepare": "husky install", "prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky install",
"postbuild": "node scripts/postbuild.js" "postbuild": "node scripts/postbuild.js"
}, },
"lint-staged": { "lint-staged": {
@ -54,13 +55,14 @@
}, },
"dependencies": { "dependencies": {
"@fontsource/inter": "4.5.7", "@fontsource/inter": "4.5.7",
"@prisma/client": "3.12.0", "@prisma/client": "3.15.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"colord": "^2.9.2", "colord": "^2.9.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"date-fns": "^2.23.0", "date-fns": "^2.23.0",
"date-fns-tz": "^1.1.4", "date-fns-tz": "^1.1.4",
"del": "^6.0.0", "del": "^6.0.0",
@ -78,7 +80,7 @@
"jose": "2.0.5", "jose": "2.0.5",
"maxmind": "^4.3.6", "maxmind": "^4.3.6",
"moment-timezone": "^0.5.33", "moment-timezone": "^0.5.33",
"next": "12.1.4", "next": "12.1.0",
"node-fetch": "^3.2.3", "node-fetch": "^3.2.3",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
@ -112,10 +114,10 @@
"postcss": "^8.4.12", "postcss": "^8.4.12",
"postcss-flexbugs-fixes": "^5.0.2", "postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^14.0.2", "postcss-import": "^14.0.2",
"postcss-preset-env": "^7.4.2", "postcss-preset-env": "7.4.3",
"postcss-rtlcss": "^3.6.1", "postcss-rtlcss": "^3.6.1",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"prisma": "3.12.0", "prisma": "3.15.2",
"prompts": "2.4.2", "prompts": "2.4.2",
"rollup": "^2.70.1", "rollup": "^2.70.1",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",

View file

@ -24,7 +24,6 @@ const Intl = ({ children }) => {
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
const { basePath } = useRouter(); const { basePath } = useRouter();
const { dir } = useLocale(); const { dir } = useLocale();
const version = process.env.VERSION;
return ( return (
<Intl> <Intl>
@ -35,12 +34,6 @@ export default function App({ Component, pageProps }) {
<link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} /> <link rel="icon" type="image/png" sizes="16x16" href={`${basePath}/favicon-16x16.png`} />
<link rel="manifest" href={`${basePath}/site.webmanifest`} /> <link rel="manifest" href={`${basePath}/site.webmanifest`} />
<link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" /> <link rel="mask-icon" href={`${basePath}/safari-pinned-tab.svg`} color="#5bbad5" />
<link
rel="preload"
href={`https://i.umami.is/icon.png?v=${version}`}
as="image"
type="image/png"
/>
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" /> <meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#2f2f2f" media="(prefers-color-scheme: dark)" />

View file

@ -6,7 +6,7 @@ function customScriptName(req) {
if (scriptName) { if (scriptName) {
const url = req.nextUrl.clone(); const url = req.nextUrl.clone();
const { pathname } = url; const { pathname } = url;
const names = scriptName.split(',').map(name => (name + '.js').trim()); const names = scriptName.split(',').map(name => name.trim() + '.js');
if (names.find(name => pathname.endsWith(name))) { if (names.find(name => pathname.endsWith(name))) {
url.pathname = '/umami.js'; url.pathname = '/umami.js';
@ -15,12 +15,6 @@ function customScriptName(req) {
} }
} }
function disableLogin(req) {
if (process.env.DISABLE_LOGIN && req.nextUrl.pathname.endsWith('/login')) {
return new Response('403 Forbidden', { status: 403 });
}
}
function forceSSL(req, res) { function forceSSL(req, res) {
if (process.env.FORCE_SSL && req.nextUrl.protocol === 'http:') { if (process.env.FORCE_SSL && req.nextUrl.protocol === 'http:') {
res.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); res.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
@ -30,7 +24,7 @@ function forceSSL(req, res) {
} }
export function middleware(req) { export function middleware(req) {
const fns = [customScriptName, disableLogin]; const fns = [customScriptName];
for (const fn of fns) { for (const fn of fns) {
const res = fn(req); const res = fn(req);

View file

@ -1,9 +1,10 @@
const { Resolver } = require('dns').promises;
import isbot from 'isbot'; import isbot from 'isbot';
import ipaddr from 'ipaddr.js'; 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 { getJsonBody, getIpAddress } from 'lib/request'; import { getJsonBody, getIpAddress } from 'lib/request';
import { ok, send, badRequest } from 'lib/response'; import { ok, send, badRequest, forbidden } from 'lib/response';
import { createToken } from 'lib/crypto'; import { createToken } from 'lib/crypto';
import { removeTrailingSlash } from 'lib/url'; import { removeTrailingSlash } from 'lib/url';
@ -15,16 +16,35 @@ export default async (req, res) => {
} }
const ignoreIps = process.env.IGNORE_IP; const ignoreIps = process.env.IGNORE_IP;
const ignoreHostnames = process.env.IGNORE_HOSTNAME;
if (ignoreIps || ignoreHostnames) {
const ips = [];
if (ignoreIps) { if (ignoreIps) {
const ips = ignoreIps.split(',').map(n => n.trim()); ips.push(...ignoreIps.split(',').map(n => n.trim()));
const ip = getIpAddress(req); }
const blocked = ips.find(i => {
if (i === ip) return true; if (ignoreHostnames) {
const resolver = new Resolver();
const promises = ignoreHostnames
.split(',')
.map(n => resolver.resolve4(n.trim()).catch(() => {}));
await Promise.all(promises).then(resolvedIps => {
ips.push(...resolvedIps.filter(n => n).flatMap(n => n));
});
}
const clientIp = getIpAddress(req);
const blocked = ips.find(ip => {
if (ip === clientIp) return true;
// CIDR notation // CIDR notation
if (i.indexOf('/') > 0) { if (ip.indexOf('/') > 0) {
const addr = ipaddr.parse(ip); const addr = ipaddr.parse(clientIp);
const range = ipaddr.parseCIDR(i); const range = ipaddr.parseCIDR(ip);
if (addr.kind() === range[0].kind() && addr.match(range)) return true; if (addr.kind() === range[0].kind() && addr.match(range)) return true;
} }
@ -33,7 +53,7 @@ export default async (req, res) => {
}); });
if (blocked) { if (blocked) {
return ok(res); return forbidden(res);
} }
} }

View file

@ -2,7 +2,7 @@ import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed, badRequest } from 'lib/response'; import { ok, methodNotAllowed, badRequest } from 'lib/response';
import { getRealtimeData } from 'lib/queries'; import { getRealtimeData } from 'lib/queries';
import { parseToken } from 'lib/crypto'; import { parseToken } from 'lib/crypto';
import { TOKEN_HEADER } from 'lib/constants'; import { SHARE_TOKEN_HEADER } from 'lib/constants';
export default async (req, res) => { export default async (req, res) => {
await useAuth(req, res); await useAuth(req, res);
@ -10,7 +10,7 @@ export default async (req, res) => {
if (req.method === 'GET') { if (req.method === 'GET') {
const { start_at } = req.query; const { start_at } = req.query;
const token = req.headers[TOKEN_HEADER]; const token = req.headers[SHARE_TOKEN_HEADER];
if (!token) { if (!token) {
return badRequest(res); return badRequest(res);

View file

@ -83,12 +83,13 @@ export default async (req, res) => {
const data = await getPageviewMetrics(websiteId, startDate, endDate, column, table, { const data = await getPageviewMetrics(websiteId, startDate, endDate, column, table, {
domain, domain,
url: type !== 'url' ? url : undefined, url: type !== 'url' && table !== 'event' ? url : undefined,
referrer: type !== 'referrer' ? referrer : undefined, referrer: type !== 'referrer' ? referrer : undefined,
os: type !== 'os' ? os : undefined, os: type !== 'os' ? os : undefined,
browser: type !== 'browser' ? browser : undefined, browser: type !== 'browser' ? browser : undefined,
device: type !== 'device' ? device : undefined, device: type !== 'device' ? device : undefined,
country: type !== 'country' ? country : undefined, country: type !== 'country' ? country : undefined,
event_url: type !== 'url' && table === 'event' ? url : undefined,
}); });
return ok(res, data); return ok(res, data);

View file

@ -3,7 +3,7 @@ import Layout from 'components/layout/Layout';
import LoginForm from 'components/forms/LoginForm'; import LoginForm from 'components/forms/LoginForm';
export default function LoginPage() { export default function LoginPage() {
if (process.env.DISABLE_LOGIN) { if (process.env.loginDisabled) {
return null; return null;
} }

View file

@ -1 +0,0 @@
../schema.mysql.prisma

View file

@ -1 +0,0 @@
../schema.postgresql.prisma

View file

@ -1,29 +0,0 @@
const bcrypt = require('bcryptjs');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const SALT_ROUNDS = 10;
const hashPassword = password => {
return bcrypt.hashSync(password, SALT_ROUNDS);
};
async function main() {
await prisma.account.upsert({
where: { username: 'admin' },
update: {},
create: {
username: 'admin',
password: hashPassword(process.env.ADMIN_PASSWORD || 'umami'),
is_admin: true,
},
});
}
main()
.catch(e => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View file

@ -0,0 +1 @@
404: Not Found

View file

@ -0,0 +1 @@
404: Not Found

View file

@ -176,7 +176,7 @@
"label.language": [ "label.language": [
{ {
"type": 0, "type": 0,
"value": "Language" "value": "Sprache"
} }
], ],
"label.last-days": [ "label.last-days": [
@ -334,7 +334,7 @@
"label.theme": [ "label.theme": [
{ {
"type": 0, "type": 0,
"value": "Theme" "value": "Thema"
} }
], ],
"label.this-month": [ "label.this-month": [
@ -704,7 +704,7 @@
"metrics.device.mobile": [ "metrics.device.mobile": [
{ {
"type": 0, "type": 0,
"value": "Mobiltelefon" "value": "Handy"
} }
], ],
"metrics.device.tablet": [ "metrics.device.tablet": [

View file

@ -38,7 +38,7 @@
"label.all-time": [ "label.all-time": [
{ {
"type": 0, "type": 0,
"value": "All time" "value": "همه زمان"
} }
], ],
"label.all-websites": [ "label.all-websites": [
@ -176,7 +176,7 @@
"label.language": [ "label.language": [
{ {
"type": 0, "type": 0,
"value": "Language" "value": "زبان"
} }
], ],
"label.last-days": [ "label.last-days": [
@ -190,7 +190,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " روز" "value": " روز گذشته"
} }
], ],
"label.last-hours": [ "label.last-hours": [
@ -204,7 +204,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " ساعت" "value": " ساعت گذشته"
} }
], ],
"label.logged-in-as": [ "label.logged-in-as": [
@ -250,7 +250,7 @@
"label.owner": [ "label.owner": [
{ {
"type": 0, "type": 0,
"value": "Owner" "value": "ایجاد شده توسط"
} }
], ],
"label.password": [ "label.password": [
@ -304,7 +304,7 @@
"label.reset-website": [ "label.reset-website": [
{ {
"type": 0, "type": 0,
"value": "Reset statistics" "value": "بازنشانی آمار"
} }
], ],
"label.save": [ "label.save": [
@ -334,7 +334,7 @@
"label.theme": [ "label.theme": [
{ {
"type": 0, "type": 0,
"value": "Theme" "value": "تم"
} }
], ],
"label.this-month": [ "label.this-month": [
@ -448,7 +448,7 @@
"message.confirm-reset": [ "message.confirm-reset": [
{ {
"type": 0, "type": 0,
"value": "Are your sure you want to reset " "value": "آیا از بازنشانی آمار "
}, },
{ {
"type": 1, "type": 1,
@ -456,7 +456,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": "'s statistics?" "value": " مطمئن هستید?"
} }
], ],
"message.copied": [ "message.copied": [
@ -580,7 +580,7 @@
"message.reset-warning": [ "message.reset-warning": [
{ {
"type": 0, "type": 0,
"value": "All statistics for this website will be deleted, but your tracking code will remain intact." "value": "تمامی آمارهای این وب‌سایت حذف خواهد شد اما tracking code بدون تغییر باقی می‌ماند."
} }
], ],
"message.save-success": [ "message.save-success": [
@ -730,7 +730,7 @@
"metrics.languages": [ "metrics.languages": [
{ {
"type": 0, "type": 0,
"value": "Languages" "value": "زبان‌ها"
} }
], ],
"metrics.operating-systems": [ "metrics.operating-systems": [

View file

@ -38,7 +38,7 @@
"label.all-time": [ "label.all-time": [
{ {
"type": 0, "type": 0,
"value": "Toutes périodes" "value": "Toutes les données"
} }
], ],
"label.all-websites": [ "label.all-websites": [
@ -86,7 +86,7 @@
"label.custom-range": [ "label.custom-range": [
{ {
"type": 0, "type": 0,
"value": "Intervalle personnalisé" "value": "Période personnalisée"
} }
], ],
"label.dashboard": [ "label.dashboard": [
@ -98,13 +98,13 @@
"label.date-range": [ "label.date-range": [
{ {
"type": 0, "type": 0,
"value": "Intervalle" "value": "Période"
} }
], ],
"label.default-date-range": [ "label.default-date-range": [
{ {
"type": 0, "type": 0,
"value": "Intervalle par défaut" "value": "Période par défaut"
} }
], ],
"label.delete": [ "label.delete": [
@ -158,7 +158,7 @@
"label.enable-share-url": [ "label.enable-share-url": [
{ {
"type": 0, "type": 0,
"value": "Activer le partage d'URL" "value": "Activer l'URL de partage"
} }
], ],
"label.invalid": [ "label.invalid": [
@ -476,13 +476,13 @@
"message.get-share-url": [ "message.get-share-url": [
{ {
"type": 0, "type": 0,
"value": "Obtenez l'URL de partage" "value": "Obtenir l'URL de partage"
} }
], ],
"message.get-tracking-code": [ "message.get-tracking-code": [
{ {
"type": 0, "type": 0,
"value": "Obtenez le code de suivi" "value": "Obtenir le code de suivi"
} }
], ],
"message.go-to-settings": [ "message.go-to-settings": [
@ -494,7 +494,7 @@
"message.incorrect-username-password": [ "message.incorrect-username-password": [
{ {
"type": 0, "type": 0,
"value": "nom d'utilisateurs/mot de passe incorrect." "value": "Nom d'utilisateur/Mot de passe incorrect."
} }
], ],
"message.log.visitor": [ "message.log.visitor": [
@ -602,7 +602,7 @@
"message.toggle-charts": [ "message.toggle-charts": [
{ {
"type": 0, "type": 0,
"value": "Changer les graphiques" "value": "Afficher/Masquer les graphiques"
} }
], ],
"message.track-stats": [ "message.track-stats": [
@ -736,7 +736,7 @@
"metrics.filter.raw": [ "metrics.filter.raw": [
{ {
"type": 0, "type": 0,
"value": "Brute" "value": "Brut"
} }
], ],
"metrics.languages": [ "metrics.languages": [
@ -766,7 +766,7 @@
"metrics.referrers": [ "metrics.referrers": [
{ {
"type": 0, "type": 0,
"value": "URL Référentes" "value": "Sources"
} }
], ],
"metrics.unique-visitors": [ "metrics.unique-visitors": [

View file

@ -0,0 +1,794 @@
{
"label.accounts": [
{
"type": 0,
"value": "Contas"
}
],
"label.add-account": [
{
"type": 0,
"value": "Engadir conta"
}
],
"label.add-website": [
{
"type": 0,
"value": "Engadir sitio web"
}
],
"label.administrator": [
{
"type": 0,
"value": "Administradora"
}
],
"label.all": [
{
"type": 0,
"value": "Todo"
}
],
"label.all-events": [
{
"type": 0,
"value": "Tódolos eventos"
}
],
"label.all-time": [
{
"type": 0,
"value": "Sempre"
}
],
"label.all-websites": [
{
"type": 0,
"value": "Tódolos sitios web"
}
],
"label.back": [
{
"type": 0,
"value": "Atrás"
}
],
"label.cancel": [
{
"type": 0,
"value": "Cancelar"
}
],
"label.change-password": [
{
"type": 0,
"value": "Mudar contrasinal"
}
],
"label.confirm-password": [
{
"type": 0,
"value": "Confirmar contrasinal"
}
],
"label.copy-to-clipboard": [
{
"type": 0,
"value": "Copiar ao portapapeis"
}
],
"label.current-password": [
{
"type": 0,
"value": "Contrasinal actual"
}
],
"label.custom-range": [
{
"type": 0,
"value": "Rango personalizado"
}
],
"label.dashboard": [
{
"type": 0,
"value": "Taboleiro"
}
],
"label.date-range": [
{
"type": 0,
"value": "Rango temporal"
}
],
"label.default-date-range": [
{
"type": 0,
"value": "Rango temporal por defecto"
}
],
"label.delete": [
{
"type": 0,
"value": "Eliminar"
}
],
"label.delete-account": [
{
"type": 0,
"value": "Eliminar conta"
}
],
"label.delete-website": [
{
"type": 0,
"value": "Eliminar sitio web"
}
],
"label.dismiss": [
{
"type": 0,
"value": "Desbotar"
}
],
"label.domain": [
{
"type": 0,
"value": "Dominio"
}
],
"label.edit": [
{
"type": 0,
"value": "Editar"
}
],
"label.edit-account": [
{
"type": 0,
"value": "Editar conta"
}
],
"label.edit-website": [
{
"type": 0,
"value": "Editar sitio web"
}
],
"label.enable-share-url": [
{
"type": 0,
"value": "Activar URL de compartición"
}
],
"label.invalid": [
{
"type": 0,
"value": "Non válido"
}
],
"label.invalid-domain": [
{
"type": 0,
"value": "Dominio non válido"
}
],
"label.language": [
{
"type": 0,
"value": "Idioma"
}
],
"label.last-days": [
{
"type": 0,
"value": "Últimos "
},
{
"type": 1,
"value": "x"
},
{
"type": 0,
"value": " días"
}
],
"label.last-hours": [
{
"type": 0,
"value": "Últimas "
},
{
"type": 1,
"value": "x"
},
{
"type": 0,
"value": " horas"
}
],
"label.logged-in-as": [
{
"type": 0,
"value": "Sesión de "
},
{
"type": 1,
"value": "username"
}
],
"label.login": [
{
"type": 0,
"value": "Acceder"
}
],
"label.logout": [
{
"type": 0,
"value": "Pechar sesión"
}
],
"label.more": [
{
"type": 0,
"value": "Máis"
}
],
"label.name": [
{
"type": 0,
"value": "Nome"
}
],
"label.new-password": [
{
"type": 0,
"value": "Novo contrasinal"
}
],
"label.owner": [
{
"type": 0,
"value": "Dona"
}
],
"label.password": [
{
"type": 0,
"value": "Contrasinal"
}
],
"label.passwords-dont-match": [
{
"type": 0,
"value": "Non concordan os contrasinais"
}
],
"label.profile": [
{
"type": 0,
"value": "Perfil"
}
],
"label.realtime": [
{
"type": 0,
"value": "Agora mesmo"
}
],
"label.realtime-logs": [
{
"type": 0,
"value": "Rexistro neste intre"
}
],
"label.refresh": [
{
"type": 0,
"value": "Actualizar"
}
],
"label.required": [
{
"type": 0,
"value": "Requerido"
}
],
"label.reset": [
{
"type": 0,
"value": "Restablecer"
}
],
"label.reset-website": [
{
"type": 0,
"value": "Restablecer estatísticas"
}
],
"label.save": [
{
"type": 0,
"value": "Gardar"
}
],
"label.settings": [
{
"type": 0,
"value": "Axustes"
}
],
"label.share-url": [
{
"type": 0,
"value": "Compartir URL"
}
],
"label.single-day": [
{
"type": 0,
"value": "Un só día"
}
],
"label.theme": [
{
"type": 0,
"value": "Decorado"
}
],
"label.this-month": [
{
"type": 0,
"value": "Este mes"
}
],
"label.this-week": [
{
"type": 0,
"value": "Esta semana"
}
],
"label.this-year": [
{
"type": 0,
"value": "Este ano"
}
],
"label.timezone": [
{
"type": 0,
"value": "Zona horaria"
}
],
"label.today": [
{
"type": 0,
"value": "Hoxe"
}
],
"label.tracking-code": [
{
"type": 0,
"value": "Código de seguimento"
}
],
"label.unknown": [
{
"type": 0,
"value": "Descoñecido"
}
],
"label.username": [
{
"type": 0,
"value": "Identificador"
}
],
"label.view-details": [
{
"type": 0,
"value": "Ver detalles"
}
],
"label.websites": [
{
"type": 0,
"value": "Sitios web"
}
],
"message.active-users": [
{
"type": 1,
"value": "x"
},
{
"type": 0,
"value": " actual "
},
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "visitante"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "visitantes"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "x"
}
],
"message.confirm-delete": [
{
"type": 0,
"value": "Tes a certeza de querer eliminar "
},
{
"type": 1,
"value": "target"
},
{
"type": 0,
"value": "?"
}
],
"message.confirm-reset": [
{
"type": 0,
"value": "Tes a certeza de querer restablecer as estatísticas de "
},
{
"type": 1,
"value": "target"
},
{
"type": 0,
"value": "?"
}
],
"message.copied": [
{
"type": 0,
"value": "Copiado!"
}
],
"message.delete-warning": [
{
"type": 0,
"value": "Tamén serán borrados tódolos datos asociados."
}
],
"message.failure": [
{
"type": 0,
"value": "Houbo un fallo."
}
],
"message.get-share-url": [
{
"type": 0,
"value": "Obter URL de compartición"
}
],
"message.get-tracking-code": [
{
"type": 0,
"value": "Obter código de seguimento"
}
],
"message.go-to-settings": [
{
"type": 0,
"value": "Ir aos axustes"
}
],
"message.incorrect-username-password": [
{
"type": 0,
"value": "Credenciais incorrectas."
}
],
"message.log.visitor": [
{
"type": 0,
"value": "Visitante desde "
},
{
"type": 1,
"value": "country"
},
{
"type": 0,
"value": " usando "
},
{
"type": 1,
"value": "browser"
},
{
"type": 0,
"value": " en "
},
{
"type": 1,
"value": "os"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "device"
}
],
"message.new-version-available": [
{
"type": 0,
"value": "A nova versión "
},
{
"type": 1,
"value": "version"
},
{
"type": 0,
"value": " de umami está dispoñible!"
}
],
"message.no-data-available": [
{
"type": 0,
"value": "Sen datos dispoñibles."
}
],
"message.no-websites-configured": [
{
"type": 0,
"value": "Non tes sitios web configurados."
}
],
"message.page-not-found": [
{
"type": 0,
"value": "Páxina non atopada."
}
],
"message.powered-by": [
{
"type": 0,
"value": "Funciona grazas a "
},
{
"type": 1,
"value": "name"
}
],
"message.reset-warning": [
{
"type": 0,
"value": "Vanse eliminar tódalas estatísticas deste sitio web, pero o código de seguimento permanecerá sen cambios."
}
],
"message.save-success": [
{
"type": 0,
"value": "Gardouse correctamente."
}
],
"message.share-url": [
{
"type": 0,
"value": "Este é o URL da compartición pública de "
},
{
"type": 1,
"value": "target"
},
{
"type": 0,
"value": "."
}
],
"message.toggle-charts": [
{
"type": 0,
"value": "Activación das gráficas"
}
],
"message.track-stats": [
{
"type": 0,
"value": "Para crear estatísticas de "
},
{
"type": 1,
"value": "target"
},
{
"type": 0,
"value": ", pon este código na sección "
},
{
"type": 1,
"value": "head"
},
{
"type": 0,
"value": " do teu sitio web."
}
],
"message.type-delete": [
{
"type": 0,
"value": "Escribe "
},
{
"type": 1,
"value": "delete"
},
{
"type": 0,
"value": " na caixa inferior para confirmar."
}
],
"message.type-reset": [
{
"type": 0,
"value": "Escribe "
},
{
"type": 1,
"value": "reset"
},
{
"type": 0,
"value": " na caixa inferior para confirmar."
}
],
"metrics.actions": [
{
"type": 0,
"value": "Accións"
}
],
"metrics.average-visit-time": [
{
"type": 0,
"value": "Tempo medio de visita"
}
],
"metrics.bounce-rate": [
{
"type": 0,
"value": "Proporción de rebote"
}
],
"metrics.browsers": [
{
"type": 0,
"value": "Navegadores"
}
],
"metrics.countries": [
{
"type": 0,
"value": "Países"
}
],
"metrics.device.desktop": [
{
"type": 0,
"value": "Escritorio"
}
],
"metrics.device.laptop": [
{
"type": 0,
"value": "Portátil"
}
],
"metrics.device.mobile": [
{
"type": 0,
"value": "Móbil"
}
],
"metrics.device.tablet": [
{
"type": 0,
"value": "Tableta"
}
],
"metrics.devices": [
{
"type": 0,
"value": "Dispositivos"
}
],
"metrics.events": [
{
"type": 0,
"value": "Eventos"
}
],
"metrics.filter.combined": [
{
"type": 0,
"value": "Combinado"
}
],
"metrics.filter.domain-only": [
{
"type": 0,
"value": "Só dominio"
}
],
"metrics.filter.raw": [
{
"type": 0,
"value": "Raw"
}
],
"metrics.languages": [
{
"type": 0,
"value": "Idiomas"
}
],
"metrics.operating-systems": [
{
"type": 0,
"value": "Sistemas operativos"
}
],
"metrics.page-views": [
{
"type": 0,
"value": "Vistas de páxinas"
}
],
"metrics.pages": [
{
"type": 0,
"value": "Páxinas"
}
],
"metrics.referrers": [
{
"type": 0,
"value": "Orixes"
}
],
"metrics.unique-visitors": [
{
"type": 0,
"value": "Visitas únicas"
}
],
"metrics.views": [
{
"type": 0,
"value": "Visualizacións"
}
],
"metrics.visitors": [
{
"type": 0,
"value": "Visitantes"
}
]
}

View file

@ -80,7 +80,7 @@
"label.current-password": [ "label.current-password": [
{ {
"type": 0, "type": 0,
"value": "Password corrente" "value": "Password attuale"
} }
], ],
"label.custom-range": [ "label.custom-range": [
@ -176,7 +176,7 @@
"label.language": [ "label.language": [
{ {
"type": 0, "type": 0,
"value": "Language" "value": "Lingua"
} }
], ],
"label.last-days": [ "label.last-days": [
@ -334,7 +334,7 @@
"label.theme": [ "label.theme": [
{ {
"type": 0, "type": 0,
"value": "Theme" "value": "Tema"
} }
], ],
"label.this-month": [ "label.this-month": [
@ -452,7 +452,7 @@
"message.confirm-reset": [ "message.confirm-reset": [
{ {
"type": 0, "type": 0,
"value": "Are your sure you want to reset " "value": "Sei sicuro di voler azzerare le statistiche di "
}, },
{ {
"type": 1, "type": 1,
@ -460,7 +460,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": "'s statistics?" "value": "?"
} }
], ],
"message.copied": [ "message.copied": [
@ -542,7 +542,7 @@
"message.new-version-available": [ "message.new-version-available": [
{ {
"type": 0, "type": 0,
"value": "Una nuova versione umami " "value": "Una nuova versione "
}, },
{ {
"type": 1, "type": 1,
@ -550,7 +550,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": " è disponibile!" "value": " di umami è disponibile!"
} }
], ],
"message.no-data-available": [ "message.no-data-available": [
@ -774,7 +774,7 @@
"metrics.referrers": [ "metrics.referrers": [
{ {
"type": 0, "type": 0,
"value": "Referr" "value": "Referrers"
} }
], ],
"metrics.unique-visitors": [ "metrics.unique-visitors": [

View file

@ -38,7 +38,7 @@
"label.all-time": [ "label.all-time": [
{ {
"type": 0, "type": 0,
"value": "All time" "value": "Бүх цаг үеийн"
} }
], ],
"label.all-websites": [ "label.all-websites": [
@ -176,7 +176,7 @@
"label.language": [ "label.language": [
{ {
"type": 0, "type": 0,
"value": "Language" "value": "Хэл"
} }
], ],
"label.last-days": [ "label.last-days": [
@ -250,7 +250,7 @@
"label.owner": [ "label.owner": [
{ {
"type": 0, "type": 0,
"value": "Owner" "value": "Эзэмшигч"
} }
], ],
"label.password": [ "label.password": [
@ -304,7 +304,7 @@
"label.reset-website": [ "label.reset-website": [
{ {
"type": 0, "type": 0,
"value": "Reset statistics" "value": "Тоон үзүүлэлтийг дахин эхлүүлэх"
} }
], ],
"label.save": [ "label.save": [
@ -334,7 +334,7 @@
"label.theme": [ "label.theme": [
{ {
"type": 0, "type": 0,
"value": "Theme" "value": "Загвар"
} }
], ],
"label.this-month": [ "label.this-month": [
@ -588,7 +588,7 @@
"message.reset-warning": [ "message.reset-warning": [
{ {
"type": 0, "type": 0,
"value": "All statistics for this website will be deleted, but your tracking code will remain intact." "value": "Энэ вебийн бүх тоон үзүүлэлтүүдийг устгах болно. Гэхдээ мөрдөх код хэвэндээ үлдэнэ."
} }
], ],
"message.save-success": [ "message.save-success": [
@ -610,7 +610,7 @@
"message.toggle-charts": [ "message.toggle-charts": [
{ {
"type": 0, "type": 0,
"value": "Toggle charts" "value": "Графикийг харуулах/нуух"
} }
], ],
"message.track-stats": [ "message.track-stats": [
@ -746,7 +746,7 @@
"metrics.languages": [ "metrics.languages": [
{ {
"type": 0, "type": 0,
"value": "Languages" "value": "Хэл"
} }
], ],
"metrics.operating-systems": [ "metrics.operating-systems": [

View file

@ -32,13 +32,13 @@
"label.all-events": [ "label.all-events": [
{ {
"type": 0, "type": 0,
"value": "All events" "value": "Todos os eventos"
} }
], ],
"label.all-time": [ "label.all-time": [
{ {
"type": 0, "type": 0,
"value": "All time" "value": "Todo o período"
} }
], ],
"label.all-websites": [ "label.all-websites": [
@ -176,7 +176,7 @@
"label.language": [ "label.language": [
{ {
"type": 0, "type": 0,
"value": "Language" "value": "Idioma"
} }
], ],
"label.last-days": [ "label.last-days": [
@ -250,7 +250,7 @@
"label.owner": [ "label.owner": [
{ {
"type": 0, "type": 0,
"value": "Owner" "value": "Proprietário"
} }
], ],
"label.password": [ "label.password": [
@ -304,7 +304,7 @@
"label.reset-website": [ "label.reset-website": [
{ {
"type": 0, "type": 0,
"value": "Reset statistics" "value": "Redefinir estatísticas"
} }
], ],
"label.save": [ "label.save": [
@ -334,7 +334,7 @@
"label.theme": [ "label.theme": [
{ {
"type": 0, "type": 0,
"value": "Theme" "value": "Tema"
} }
], ],
"label.this-month": [ "label.this-month": [
@ -452,7 +452,7 @@
"message.confirm-reset": [ "message.confirm-reset": [
{ {
"type": 0, "type": 0,
"value": "Are your sure you want to reset " "value": "Você tem certeza que deseja redefinir as estatísticas de "
}, },
{ {
"type": 1, "type": 1,
@ -460,7 +460,7 @@
}, },
{ {
"type": 0, "type": 0,
"value": "'s statistics?" "value": "?"
} }
], ],
"message.copied": [ "message.copied": [
@ -584,7 +584,7 @@
"message.reset-warning": [ "message.reset-warning": [
{ {
"type": 0, "type": 0,
"value": "All statistics for this website will be deleted, but your tracking code will remain intact." "value": "Todas as estatísticas deste site serão removidas, mas seu código de rastreamento permanecerá intacto."
} }
], ],
"message.save-success": [ "message.save-success": [
@ -610,7 +610,7 @@
"message.toggle-charts": [ "message.toggle-charts": [
{ {
"type": 0, "type": 0,
"value": "Toggle charts" "value": "Mostrar/Esconder gráficos"
} }
], ],
"message.track-stats": [ "message.track-stats": [
@ -750,7 +750,7 @@
"metrics.languages": [ "metrics.languages": [
{ {
"type": 0, "type": 0,
"value": "Languages" "value": "Idiomas"
} }
], ],
"metrics.operating-systems": [ "metrics.operating-systems": [

View file

@ -176,7 +176,7 @@
"label.language": [ "label.language": [
{ {
"type": 0, "type": 0,
"value": "Language" "value": "Язык"
} }
], ],
"label.last-days": [ "label.last-days": [
@ -334,7 +334,7 @@
"label.theme": [ "label.theme": [
{ {
"type": 0, "type": 0,
"value": "Theme" "value": "Тема"
} }
], ],
"label.this-month": [ "label.this-month": [

View file

@ -20,7 +20,7 @@
"label.administrator": [ "label.administrator": [
{ {
"type": 0, "type": 0,
"value": "Quản Trị" "value": "Quản trị"
} }
], ],
"label.all": [ "label.all": [
@ -32,19 +32,19 @@
"label.all-events": [ "label.all-events": [
{ {
"type": 0, "type": 0,
"value": "Tất cả events" "value": "Tất cả sự kiện"
} }
], ],
"label.all-time": [ "label.all-time": [
{ {
"type": 0, "type": 0,
"value": "All time" "value": "Toàn thời gian"
} }
], ],
"label.all-websites": [ "label.all-websites": [
{ {
"type": 0, "type": 0,
"value": "Tất cả websites" "value": "Tất cả website"
} }
], ],
"label.back": [ "label.back": [
@ -104,7 +104,7 @@
"label.default-date-range": [ "label.default-date-range": [
{ {
"type": 0, "type": 0,
"value": "Phạm vi ngày mặc định" "value": "Khoảng thời gian mặc định"
} }
], ],
"label.delete": [ "label.delete": [
@ -242,7 +242,7 @@
"label.owner": [ "label.owner": [
{ {
"type": 0, "type": 0,
"value": "Owner" "value": "Chủ nhân"
} }
], ],
"label.password": [ "label.password": [
@ -590,7 +590,7 @@
"message.toggle-charts": [ "message.toggle-charts": [
{ {
"type": 0, "type": 0,
"value": "Toggle charts" "value": "Bật/tắt biểu đồ"
} }
], ],
"message.track-stats": [ "message.track-stats": [
@ -730,7 +730,7 @@
"metrics.languages": [ "metrics.languages": [
{ {
"type": 0, "type": 0,
"value": "Languages" "value": "Ngôn ngũ"
} }
], ],
"metrics.operating-systems": [ "metrics.operating-systems": [
@ -760,7 +760,7 @@
"metrics.unique-visitors": [ "metrics.unique-visitors": [
{ {
"type": 0, "type": 0,
"value": "Khách truy cập duy nhất" "value": "Khách truy cập một lần"
} }
], ],
"metrics.views": [ "metrics.views": [

100
scripts/check-db.js Normal file
View file

@ -0,0 +1,100 @@
require('dotenv').config();
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const chalk = require('chalk');
const spawn = require('cross-spawn');
let message = '';
const updateMessage = `To update your database, you need to run:\n${chalk.bold.whiteBright(
'yarn update-db',
)}`;
const baselineMessage = cmd =>
`You need to update your database by running:\n${chalk.bold.whiteBright(cmd)}`;
function success(msg) {
console.log(chalk.greenBright(`${msg}`));
}
async function checkEnv() {
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL is not defined.');
} else {
success('DATABASE_URL is defined.');
}
}
async function checkConnection() {
try {
await prisma.$connect();
success('Database connection successful.');
} catch (e) {
throw new Error('Unable to connect to the database.');
}
}
async function checkTables() {
try {
await prisma.account.findFirst();
success('Database tables found.');
} catch (e) {
message = updateMessage;
throw new Error('Database tables not found.');
}
}
async function run(cmd, args) {
const buffer = [];
const proc = spawn(cmd, args);
return new Promise((resolve, reject) => {
proc.stdout.on('data', data => buffer.push(data));
proc.on('error', () => {
reject(new Error('Failed to run Prisma.'));
});
proc.on('exit', () => resolve(buffer.join('')));
});
}
async function checkMigrations() {
const output = await run('prisma', ['migrate', 'status']);
const missingMigrations = output.includes('Following migration have not yet been applied');
const notManaged = output.includes('The current database is not managed');
if (notManaged) {
const cmd = output.match(/yarn prisma migrate resolve --applied ".*"/g);
message = baselineMessage(cmd[0]);
throw new Error('Database is out of date.');
} else if (missingMigrations) {
message = updateMessage;
throw new Error('Database is out of date.');
}
success('Database is up to date.');
}
(async () => {
let err = false;
for (let fn of [checkEnv, checkConnection, checkTables, checkMigrations]) {
try {
await fn();
} catch (e) {
console.log(chalk.red(`${e.message}`));
err = true;
} finally {
await prisma.$disconnect();
if (err) {
console.log(message);
process.exit(1);
}
}
}
})();

View file

@ -1,8 +1,9 @@
require('dotenv').config(); require('dotenv').config();
const fs = require('fs'); const fse = require('fs-extra');
const path = require('path'); const path = require('path');
const del = require('del');
function getDatabase() { function getDatabaseType() {
const type = const type =
process.env.DATABASE_TYPE || process.env.DATABASE_TYPE ||
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]); (process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
@ -14,7 +15,7 @@ function getDatabase() {
return type; return type;
} }
const databaseType = getDatabase(); const databaseType = getDatabaseType();
if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) { if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
throw new Error('Missing or invalid database'); throw new Error('Missing or invalid database');
@ -22,9 +23,11 @@ if (!databaseType || !['mysql', 'postgresql'].includes(databaseType)) {
console.log(`Database type detected: ${databaseType}`); console.log(`Database type detected: ${databaseType}`);
const src = path.resolve(__dirname, `../prisma/schema.${databaseType}.prisma`); const src = path.resolve(__dirname, `../db/${databaseType}`);
const dest = path.resolve(__dirname, '../prisma/schema.prisma'); const dest = path.resolve(__dirname, '../prisma');
fs.copyFileSync(src, dest); del.sync(dest);
fse.copySync(src, dest);
console.log(`Copied ${src} to ${dest}`); console.log(`Copied ${src} to ${dest}`);

View file

@ -4,7 +4,7 @@ const https = require('https');
const chalk = require('chalk'); const chalk = require('chalk');
const src = path.resolve(__dirname, '../lang'); const src = path.resolve(__dirname, '../lang');
const dest = path.resolve(__dirname, '../public/country'); const dest = path.resolve(__dirname, '../public/intl/country');
const files = fs.readdirSync(src); const files = fs.readdirSync(src);
const getUrl = locale => const getUrl = locale =>

View file

@ -4,7 +4,7 @@ const https = require('https');
const chalk = require('chalk'); const chalk = require('chalk');
const src = path.resolve(__dirname, '../lang'); const src = path.resolve(__dirname, '../lang');
const dest = path.resolve(__dirname, '../public/language'); const dest = path.resolve(__dirname, '../public/intl/language');
const files = fs.readdirSync(src); const files = fs.readdirSync(src);
const getUrl = locale => const getUrl = locale =>

View file

@ -4,12 +4,13 @@ import semver from 'semver';
import { VERSION_CHECK } from 'lib/constants'; import { VERSION_CHECK } from 'lib/constants';
import { getItem } from 'lib/web'; import { getItem } from 'lib/web';
const REPO_URL = 'https://api.github.com/repos/mikecao/umami/releases/latest'; const UPDATES_URL = 'https://api.umami.is/v1/updates';
const initialState = { const initialState = {
current: process.env.VERSION, current: process.env.currentVersion,
latest: null, latest: null,
hasUpdate: false, hasUpdate: false,
checked: false,
}; };
const store = create(() => ({ ...initialState })); const store = create(() => ({ ...initialState }));
@ -17,10 +18,10 @@ const store = create(() => ({ ...initialState }));
export async function checkVersion() { export async function checkVersion() {
const { current } = store.getState(); const { current } = store.getState();
const data = await fetch(REPO_URL, { const data = await fetch(`${UPDATES_URL}?v=${current}`, {
method: 'get', method: 'GET',
headers: { headers: {
Accept: 'application/vnd.github.v3+json', Accept: 'application/json',
}, },
}).then(res => { }).then(res => {
if (res.ok) { if (res.ok) {
@ -36,15 +37,15 @@ export async function checkVersion() {
store.setState( store.setState(
produce(state => { produce(state => {
const { tag_name } = data; const { latest } = data;
const latest = tag_name.startsWith('v') ? tag_name.slice(1) : tag_name;
const lastCheck = getItem(VERSION_CHECK); const lastCheck = getItem(VERSION_CHECK);
const hasUpdate = latest && semver.gt(latest, current) && lastCheck?.version !== latest;
const hasUpdate = !!(latest && lastCheck?.version !== latest && semver.gt(latest, current));
state.current = current; state.current = current;
state.latest = latest; state.latest = latest;
state.hasUpdate = hasUpdate; state.hasUpdate = hasUpdate;
state.checked = true;
return state; return state;
}), }),

View file

@ -122,7 +122,11 @@ import { removeTrailingSlash } from '../lib/url';
payload, payload,
}); });
navigator.sendBeacon(`${root}/api/collect`, data); fetch(`${root}/api/collect`, {
method: 'POST',
body: data,
keepalive: true,
});
}; };
const addEvents = node => { const addEvents = node => {

1414
yarn.lock

File diff suppressed because it is too large Load diff