mirror of
https://github.com/umami-software/umami.git
synced 2026-02-06 05:37:20 +01:00
Merge branch 'master' into hosts-support
This commit is contained in:
commit
e11c2e452c
69 changed files with 3783 additions and 2197 deletions
|
|
@ -1,82 +0,0 @@
|
|||
import { User, Website } from '@prisma/client';
|
||||
import redis from '@umami/redis-client';
|
||||
import { getSession, getUser, getWebsite } from '../queries';
|
||||
|
||||
async function fetchWebsite(websiteId: string): Promise<Website> {
|
||||
return redis.client.getCache(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
|
||||
}
|
||||
|
||||
async function storeWebsite(data: { id: any }) {
|
||||
const { id } = data;
|
||||
const key = `website:${id}`;
|
||||
|
||||
const obj = await redis.client.setCache(key, data);
|
||||
await redis.client.expire(key, 86400);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function deleteWebsite(id) {
|
||||
return redis.client.deleteCache(`website:${id}`);
|
||||
}
|
||||
|
||||
async function fetchUser(id): Promise<User> {
|
||||
return redis.client.getCache(`user:${id}`, () => getUser(id, { includePassword: true }), 86400);
|
||||
}
|
||||
|
||||
async function storeUser(data) {
|
||||
const { id } = data;
|
||||
const key = `user:${id}`;
|
||||
|
||||
const obj = await redis.client.setCache(key, data);
|
||||
await redis.client.expire(key, 86400);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function deleteUser(id) {
|
||||
return redis.client.deleteCache(`user:${id}`);
|
||||
}
|
||||
|
||||
async function fetchSession(id) {
|
||||
return redis.client.getCache(`session:${id}`, () => getSession(id), 86400);
|
||||
}
|
||||
|
||||
async function storeSession(data) {
|
||||
const { id } = data;
|
||||
const key = `session:${id}`;
|
||||
|
||||
const obj = await redis.client.setCache(key, data);
|
||||
await redis.client.expire(key, 86400);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function deleteSession(id) {
|
||||
return redis.client.deleteCache(`session:${id}`);
|
||||
}
|
||||
|
||||
async function fetchUserBlock(userId: string) {
|
||||
const key = `user:block:${userId}`;
|
||||
return redis.client.get(key);
|
||||
}
|
||||
|
||||
async function incrementUserBlock(userId: string) {
|
||||
const key = `user:block:${userId}`;
|
||||
return redis.client.incr(key);
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchWebsite,
|
||||
storeWebsite,
|
||||
deleteWebsite,
|
||||
fetchUser,
|
||||
storeUser,
|
||||
deleteUser,
|
||||
fetchSession,
|
||||
storeSession,
|
||||
deleteSession,
|
||||
fetchUserBlock,
|
||||
incrementUserBlock,
|
||||
enabled: !!redis.enabled,
|
||||
};
|
||||
|
|
@ -4,7 +4,7 @@ import debug from 'debug';
|
|||
import { CLICKHOUSE } from 'lib/db';
|
||||
import { QueryFilters, QueryOptions } from './types';
|
||||
import { OPERATORS } from './constants';
|
||||
import { loadWebsite } from './load';
|
||||
import { fetchWebsite } from './load';
|
||||
import { maxDate } from './date';
|
||||
import { filtersToArray } from './params';
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ function getFilterParams(filters: QueryFilters = {}) {
|
|||
}
|
||||
|
||||
async function parseFilters(websiteId: string, filters: QueryFilters = {}, options?: QueryOptions) {
|
||||
const website = await loadWebsite(websiteId);
|
||||
const website = await fetchWebsite(websiteId);
|
||||
|
||||
return {
|
||||
filterQuery: getFilterQuery(filters, options),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ export const FILTER_DAY = 'filter-day';
|
|||
export const FILTER_RANGE = 'filter-range';
|
||||
export const FILTER_REFERRERS = 'filter-referrers';
|
||||
export const FILTER_PAGES = 'filter-pages';
|
||||
export const UNIT_TYPES = ['year', 'month', 'hour', 'day'];
|
||||
|
||||
export const UNIT_TYPES = ['year', 'month', 'hour', 'day', 'minute'];
|
||||
export const EVENT_COLUMNS = ['url', 'referrer', 'title', 'query', 'event', 'host'];
|
||||
|
||||
export const SESSION_COLUMNS = [
|
||||
|
|
@ -134,6 +135,7 @@ export const ROLES = {
|
|||
user: 'user',
|
||||
viewOnly: 'view-only',
|
||||
teamOwner: 'team-owner',
|
||||
teamManager: 'team-manager',
|
||||
teamMember: 'team-member',
|
||||
teamViewOnly: 'team-view-only',
|
||||
} as const;
|
||||
|
|
@ -164,6 +166,12 @@ export const ROLE_PERMISSIONS = {
|
|||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
],
|
||||
[ROLES.teamManager]: [
|
||||
PERMISSIONS.teamUpdate,
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
PERMISSIONS.websiteDelete,
|
||||
],
|
||||
[ROLES.teamMember]: [
|
||||
PERMISSIONS.websiteCreate,
|
||||
PERMISSIONS.websiteUpdate,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { startOfHour, startOfMonth } from 'date-fns';
|
||||
import { hash } from 'next-basics';
|
||||
import { v4, v5, validate } from 'uuid';
|
||||
import { v4, v5 } from 'uuid';
|
||||
|
||||
export function secret() {
|
||||
return hash(process.env.APP_SECRET || process.env.DATABASE_URL);
|
||||
|
|
@ -23,7 +23,3 @@ export function uuid(...args: any) {
|
|||
|
||||
return v5(hash(...args, salt()), v5.DNS);
|
||||
}
|
||||
|
||||
export function isUuid(value: string) {
|
||||
return validate(value);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export async function getClientInfo(req: NextApiRequestCollect) {
|
|||
const subdivision2 = location?.subdivision2;
|
||||
const city = location?.city;
|
||||
const browser = browserName(userAgent);
|
||||
const os = detectOS(userAgent);
|
||||
const os = detectOS(userAgent) as string;
|
||||
const device = getDevice(req.body?.payload?.screen, os);
|
||||
|
||||
return { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device };
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
arSA,
|
||||
be,
|
||||
bn,
|
||||
bs,
|
||||
cs,
|
||||
sk,
|
||||
da,
|
||||
|
|
@ -48,6 +49,7 @@ export const languages = {
|
|||
'ar-SA': { label: 'العربية', dateLocale: arSA, dir: 'rtl' },
|
||||
'be-BY': { label: 'Беларуская', dateLocale: be },
|
||||
'bn-BD': { label: 'বাংলা', dateLocale: bn },
|
||||
'bs-BA': { label: 'Bosanski', dateLocale: bs },
|
||||
'ca-ES': { label: 'Català', dateLocale: ca },
|
||||
'cs-CZ': { label: 'Čeština', dateLocale: cs },
|
||||
'da-DK': { label: 'Dansk', dateLocale: da },
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import cache from 'lib/cache';
|
||||
import { getSession, getUser, getWebsite } from 'queries';
|
||||
import { User, Website, Session } from '@prisma/client';
|
||||
import { getSession, getWebsite } from 'queries';
|
||||
import { Website, Session } from '@prisma/client';
|
||||
import redis from '@umami/redis-client';
|
||||
|
||||
export async function loadWebsite(websiteId: string): Promise<Website> {
|
||||
export async function fetchWebsite(websiteId: string): Promise<Website> {
|
||||
let website;
|
||||
|
||||
if (cache.enabled) {
|
||||
website = await cache.fetchWebsite(websiteId);
|
||||
if (redis.enabled) {
|
||||
website = await redis.client.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
|
||||
} else {
|
||||
website = await getWebsite(websiteId);
|
||||
}
|
||||
|
|
@ -18,11 +18,11 @@ export async function loadWebsite(websiteId: string): Promise<Website> {
|
|||
return website;
|
||||
}
|
||||
|
||||
export async function loadSession(sessionId: string): Promise<Session> {
|
||||
export async function fetchSession(sessionId: string): Promise<Session> {
|
||||
let session;
|
||||
|
||||
if (cache.enabled) {
|
||||
session = await cache.fetchSession(sessionId);
|
||||
if (redis.enabled) {
|
||||
session = await redis.client.fetch(`session:${sessionId}`, () => getSession(sessionId), 86400);
|
||||
} else {
|
||||
session = await getSession(sessionId);
|
||||
}
|
||||
|
|
@ -33,19 +33,3 @@ export async function loadSession(sessionId: string): Promise<Session> {
|
|||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function loadUser(userId: string): Promise<User> {
|
||||
let user;
|
||||
|
||||
if (cache.enabled) {
|
||||
user = await cache.fetchUser(userId);
|
||||
} else {
|
||||
user = await getUser(userId);
|
||||
}
|
||||
|
||||
if (!user || user.deletedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ import redis from '@umami/redis-client';
|
|||
import { getAuthToken, parseShareToken } from 'lib/auth';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { secret } from 'lib/crypto';
|
||||
import { findSession } from 'lib/session';
|
||||
import { getSession } from 'lib/session';
|
||||
import {
|
||||
badRequest,
|
||||
createMiddleware,
|
||||
forbidden,
|
||||
notFound,
|
||||
parseSecureToken,
|
||||
tooManyRequest,
|
||||
unauthorized,
|
||||
} from 'next-basics';
|
||||
import { NextApiRequestCollect } from 'pages/api/send';
|
||||
|
|
@ -27,7 +26,7 @@ export const useCors = createMiddleware(
|
|||
|
||||
export const useSession = createMiddleware(async (req, res, next) => {
|
||||
try {
|
||||
const session = await findSession(req as NextApiRequestCollect);
|
||||
const session = await getSession(req as NextApiRequestCollect);
|
||||
|
||||
if (!session) {
|
||||
log('useSession: Session not found');
|
||||
|
|
@ -36,11 +35,8 @@ export const useSession = createMiddleware(async (req, res, next) => {
|
|||
|
||||
(req as any).session = session;
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Usage Limit.') {
|
||||
return tooManyRequest(res, e.message);
|
||||
}
|
||||
if (e.message.startsWith('Website not found:')) {
|
||||
return forbidden(res, e.message);
|
||||
if (e.message.startsWith('Website not found')) {
|
||||
return notFound(res, e.message);
|
||||
}
|
||||
return badRequest(res, e.message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import prisma from '@umami/prisma-client';
|
|||
import moment from 'moment-timezone';
|
||||
import { MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
|
||||
import { SESSION_COLUMNS, OPERATORS, DEFAULT_PAGE_SIZE } from './constants';
|
||||
import { loadWebsite } from './load';
|
||||
import { fetchWebsite } from './load';
|
||||
import { maxDate } from './date';
|
||||
import { QueryFilters, QueryOptions, SearchFilter } from './types';
|
||||
import { QueryFilters, QueryOptions, PageParams } from './types';
|
||||
import { filtersToArray } from './params';
|
||||
|
||||
const MYSQL_DATE_FORMATS = {
|
||||
|
|
@ -152,7 +152,7 @@ async function parseFilters(
|
|||
filters: QueryFilters = {},
|
||||
options: QueryOptions = {},
|
||||
) {
|
||||
const website = await loadWebsite(websiteId);
|
||||
const website = await fetchWebsite(websiteId);
|
||||
const joinSession = Object.keys(filters).find(key => SESSION_COLUMNS.includes(key));
|
||||
|
||||
return {
|
||||
|
|
@ -191,7 +191,7 @@ async function rawQuery(sql: string, data: object): Promise<any> {
|
|||
return prisma.rawQuery(query, params);
|
||||
}
|
||||
|
||||
async function pagedQuery<T>(model: string, criteria: T, filters: SearchFilter) {
|
||||
async function pagedQuery<T>(model: string, criteria: T, filters: PageParams) {
|
||||
const { page = 1, pageSize, orderBy, sortDescending = false } = filters || {};
|
||||
const size = +pageSize || DEFAULT_PAGE_SIZE;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as yup from 'yup';
|
|||
|
||||
export const dateRange = {
|
||||
startAt: yup.number().integer().required(),
|
||||
endAt: yup.number().integer().moreThan(yup.ref('startAt')).required(),
|
||||
endAt: yup.number().integer().min(yup.ref('startAt')).required(),
|
||||
};
|
||||
|
||||
export const pageInfo = {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,13 @@
|
|||
import { isUuid, secret, uuid, visitSalt } from 'lib/crypto';
|
||||
import { secret, uuid, visitSalt } from 'lib/crypto';
|
||||
import { getClientInfo } from 'lib/detect';
|
||||
import { parseToken } from 'next-basics';
|
||||
import { NextApiRequestCollect } from 'pages/api/send';
|
||||
import { createSession } from 'queries';
|
||||
import cache from './cache';
|
||||
import clickhouse from './clickhouse';
|
||||
import { loadSession, loadWebsite } from './load';
|
||||
import { fetchSession, fetchWebsite } from './load';
|
||||
import { SessionData } from 'lib/types';
|
||||
|
||||
export async function findSession(req: NextApiRequestCollect): Promise<{
|
||||
id: any;
|
||||
websiteId: string;
|
||||
visitId: string;
|
||||
hostname: string;
|
||||
browser: string;
|
||||
os: any;
|
||||
device: string;
|
||||
screen: string;
|
||||
language: string;
|
||||
country: any;
|
||||
subdivision1: any;
|
||||
subdivision2: any;
|
||||
city: any;
|
||||
ownerId: string;
|
||||
}> {
|
||||
export async function getSession(req: NextApiRequestCollect): Promise<SessionData> {
|
||||
const { payload } = req.body;
|
||||
|
||||
if (!payload) {
|
||||
|
|
@ -35,9 +20,8 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
if (cacheToken) {
|
||||
const result = await parseToken(cacheToken, secret());
|
||||
|
||||
// Token is valid
|
||||
if (result) {
|
||||
await checkUserBlock(result?.ownerId);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -45,25 +29,13 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
// Verify payload
|
||||
const { website: websiteId, hostname, screen, language } = payload;
|
||||
|
||||
// Check the hostname value for legality to eliminate dirty data
|
||||
const validHostnameRegex = /^[\w-.]+$/;
|
||||
if (!validHostnameRegex.test(hostname)) {
|
||||
throw new Error('Invalid hostname.');
|
||||
}
|
||||
|
||||
if (!isUuid(websiteId)) {
|
||||
throw new Error('Invalid website ID.');
|
||||
}
|
||||
|
||||
// Find website
|
||||
const website = await loadWebsite(websiteId);
|
||||
const website = await fetchWebsite(websiteId);
|
||||
|
||||
if (!website) {
|
||||
throw new Error(`Website not found: ${websiteId}.`);
|
||||
}
|
||||
|
||||
await checkUserBlock(website.userId);
|
||||
|
||||
const { userAgent, browser, os, ip, country, subdivision1, subdivision2, city, device } =
|
||||
await getClientInfo(req);
|
||||
|
||||
|
|
@ -78,7 +50,7 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
visitId,
|
||||
hostname,
|
||||
browser,
|
||||
os: os as any,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
|
|
@ -86,12 +58,11 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
subdivision1,
|
||||
subdivision2,
|
||||
city,
|
||||
ownerId: website.userId,
|
||||
};
|
||||
}
|
||||
|
||||
// Find session
|
||||
let session = await loadSession(sessionId);
|
||||
let session = await fetchSession(sessionId);
|
||||
|
||||
// Create a session if not found
|
||||
if (!session) {
|
||||
|
|
@ -117,13 +88,5 @@ export async function findSession(req: NextApiRequestCollect): Promise<{
|
|||
}
|
||||
}
|
||||
|
||||
return { ...session, ownerId: website.userId, visitId: visitId };
|
||||
}
|
||||
|
||||
async function checkUserBlock(userId: string) {
|
||||
if (process.env.ENABLE_BLOCKER && (await cache.fetchUserBlock(userId))) {
|
||||
await cache.incrementUserBlock(userId);
|
||||
|
||||
throw new Error('Usage Limit.');
|
||||
}
|
||||
return { ...session, visitId: visitId };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,31 +24,7 @@ export type DynamicDataType = ObjectValues<typeof DATA_TYPE>;
|
|||
export type KafkaTopic = ObjectValues<typeof KAFKA_TOPIC>;
|
||||
export type ReportType = ObjectValues<typeof REPORT_TYPES>;
|
||||
|
||||
export interface WebsiteSearchFilter extends SearchFilter {
|
||||
userId?: string;
|
||||
teamId?: string;
|
||||
includeTeams?: boolean;
|
||||
onlyTeams?: boolean;
|
||||
}
|
||||
|
||||
export interface UserSearchFilter extends SearchFilter {
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export interface TeamSearchFilter extends SearchFilter {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface TeamUserSearchFilter extends SearchFilter {
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export interface ReportSearchFilter extends SearchFilter {
|
||||
userId?: string;
|
||||
websiteId?: string;
|
||||
}
|
||||
|
||||
export interface SearchFilter {
|
||||
export interface PageParams {
|
||||
query?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
|
@ -56,7 +32,7 @@ export interface SearchFilter {
|
|||
sortDescending?: boolean;
|
||||
}
|
||||
|
||||
export interface FilterResult<T> {
|
||||
export interface PageResult<T> {
|
||||
data: T;
|
||||
count: number;
|
||||
page: number;
|
||||
|
|
@ -66,10 +42,10 @@ export interface FilterResult<T> {
|
|||
}
|
||||
|
||||
export interface FilterQueryResult<T> {
|
||||
result: FilterResult<T>;
|
||||
result: PageResult<T>;
|
||||
query: any;
|
||||
params: SearchFilter;
|
||||
setParams: Dispatch<SetStateAction<T | SearchFilter>>;
|
||||
params: PageParams;
|
||||
setParams: Dispatch<SetStateAction<T | PageParams>>;
|
||||
}
|
||||
|
||||
export interface DynamicData {
|
||||
|
|
@ -230,3 +206,19 @@ export interface RealtimeData {
|
|||
countries?: any[];
|
||||
visitors?: any[];
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
id: string;
|
||||
websiteId: string;
|
||||
visitId: string;
|
||||
hostname: string;
|
||||
browser: string;
|
||||
os: string;
|
||||
device: string;
|
||||
screen: string;
|
||||
language: string;
|
||||
country: string;
|
||||
subdivision1: string;
|
||||
subdivision2: string;
|
||||
city: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue