mirror of
https://github.com/umami-software/umami.git
synced 2026-02-14 17:45:38 +01:00
Merge branch 'dev' into analytics
This commit is contained in:
commit
f4f09900e9
13 changed files with 232 additions and 206 deletions
1
.github/workflows/cd-cloud.yml
vendored
1
.github/workflows/cd-cloud.yml
vendored
|
|
@ -4,6 +4,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- analytics
|
- analytics
|
||||||
|
- cloud
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
"@react-spring/web": "^9.7.3",
|
"@react-spring/web": "^9.7.3",
|
||||||
"@tanstack/react-query": "^5.28.6",
|
"@tanstack/react-query": "^5.28.6",
|
||||||
"@umami/prisma-client": "^0.14.0",
|
"@umami/prisma-client": "^0.14.0",
|
||||||
"@umami/redis-client": "^0.24.0",
|
"@umami/redis-client": "^0.25.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"chart.js": "^4.4.2",
|
"chart.js": "^4.4.2",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { checkPassword } from '@/lib/auth';
|
import { checkPassword } from '@/lib/auth';
|
||||||
import { createSecureToken } from '@/lib/jwt';
|
import { createSecureToken } from '@/lib/jwt';
|
||||||
import { redisEnabled } from '@umami/redis-client';
|
import redis from '@/lib/redis';
|
||||||
import { getUserByUsername } from '@/queries';
|
import { getUserByUsername } from '@/queries';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
|
|
@ -29,15 +29,16 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redisEnabled) {
|
|
||||||
const token = await saveAuth({ userId: user.id });
|
|
||||||
|
|
||||||
return json({ token, user });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = createSecureToken({ userId: user.id }, secret());
|
|
||||||
const { id, role, createdAt } = user;
|
const { id, role, createdAt } = user;
|
||||||
|
|
||||||
|
let token: string;
|
||||||
|
|
||||||
|
if (redis.enabled) {
|
||||||
|
token = await saveAuth({ userId: id, role });
|
||||||
|
} else {
|
||||||
|
token = createSecureToken({ userId: user.id, role }, secret());
|
||||||
|
}
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
token,
|
token,
|
||||||
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin },
|
user: { id, username, role, createdAt, isAdmin: role === ROLES.admin },
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { getClient, redisEnabled } from '@umami/redis-client';
|
import redis from '@/lib/redis';
|
||||||
import { ok } from '@/lib/response';
|
import { ok } from '@/lib/response';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
if (redisEnabled) {
|
if (redis.enabled) {
|
||||||
const redis = getClient();
|
|
||||||
|
|
||||||
const token = request.headers.get('authorization')?.split(' ')?.[1];
|
const token = request.headers.get('authorization')?.split(' ')?.[1];
|
||||||
|
|
||||||
await redis.del(token);
|
await redis.client.del(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok();
|
return ok();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { redisEnabled } from '@umami/redis-client';
|
import redis from '@/lib/redis';
|
||||||
import { json } from '@/lib/response';
|
import { json } from '@/lib/response';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { saveAuth } from '@/lib/auth';
|
import { saveAuth } from '@/lib/auth';
|
||||||
|
|
@ -10,7 +10,7 @@ export async function POST(request: Request) {
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redisEnabled) {
|
if (redis.enabled) {
|
||||||
const token = await saveAuth({ userId: auth.user.id }, 86400);
|
const token = await saveAuth({ userId: auth.user.id }, 86400);
|
||||||
|
|
||||||
return json({ user: auth.user, token });
|
return json({ user: auth.user, token });
|
||||||
|
|
|
||||||
|
|
@ -9,190 +9,195 @@ import { getClientInfo, hasBlockedIp } from '@/lib/detect';
|
||||||
import { secret, uuid, visitSalt } from '@/lib/crypto';
|
import { secret, uuid, visitSalt } from '@/lib/crypto';
|
||||||
import { COLLECTION_TYPE } from '@/lib/constants';
|
import { COLLECTION_TYPE } from '@/lib/constants';
|
||||||
import { createSession, saveEvent, saveSessionData } from '@/queries';
|
import { createSession, saveEvent, saveSessionData } from '@/queries';
|
||||||
|
import { urlOrPathParam } from '@/lib/schema';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
type: z.enum(['event', 'identify']),
|
||||||
|
payload: z.object({
|
||||||
|
website: z.string().uuid(),
|
||||||
|
data: z.object({}).passthrough().optional(),
|
||||||
|
hostname: z.string().max(100).optional(),
|
||||||
|
language: z.string().max(35).optional(),
|
||||||
|
referrer: urlOrPathParam,
|
||||||
|
screen: z.string().max(11).optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
url: urlOrPathParam,
|
||||||
|
name: z.string().url().max(50).optional(),
|
||||||
|
tag: z.string().max(50).optional(),
|
||||||
|
ip: z.string().ip().optional(),
|
||||||
|
userAgent: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// Bot check
|
try {
|
||||||
if (!process.env.DISABLE_BOT_CHECK && isbot(request.headers.get('user-agent'))) {
|
// Bot check
|
||||||
return json({ beep: 'boop' });
|
if (!process.env.DISABLE_BOT_CHECK && isbot(request.headers.get('user-agent'))) {
|
||||||
}
|
return json({ beep: 'boop' });
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
type: z.enum(['event', 'identify']),
|
|
||||||
payload: z.object({
|
|
||||||
website: z.string().uuid(),
|
|
||||||
data: z.object({}).passthrough().optional(),
|
|
||||||
hostname: z.string().max(100).optional(),
|
|
||||||
language: z.string().max(35).optional(),
|
|
||||||
referrer: z.string().optional(),
|
|
||||||
screen: z.string().max(11).optional(),
|
|
||||||
title: z.string().optional(),
|
|
||||||
url: z.string().optional(),
|
|
||||||
name: z.string().max(50).optional(),
|
|
||||||
tag: z.string().max(50).optional(),
|
|
||||||
ip: z.string().ip().optional(),
|
|
||||||
userAgent: z.string().optional(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { body, error } = await parseRequest(request, schema, { skipAuth: true });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return error();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type, payload } = body;
|
|
||||||
|
|
||||||
const {
|
|
||||||
website: websiteId,
|
|
||||||
hostname,
|
|
||||||
screen,
|
|
||||||
language,
|
|
||||||
url,
|
|
||||||
referrer,
|
|
||||||
name,
|
|
||||||
data,
|
|
||||||
title,
|
|
||||||
tag,
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
// Cache check
|
|
||||||
let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null;
|
|
||||||
const cacheHeader = request.headers.get('x-umami-cache');
|
|
||||||
|
|
||||||
if (cacheHeader) {
|
|
||||||
const result = await parseToken(cacheHeader, secret());
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
cache = result;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Find website
|
const { body, error } = await parseRequest(request, schema, { skipAuth: true });
|
||||||
if (!cache?.websiteId) {
|
|
||||||
const website = await fetchWebsite(websiteId);
|
|
||||||
|
|
||||||
if (!website) {
|
if (error) {
|
||||||
return badRequest('Website not found.');
|
return error();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Client info
|
const { type, payload } = body;
|
||||||
const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } =
|
|
||||||
await getClientInfo(request, payload);
|
|
||||||
|
|
||||||
// IP block
|
const {
|
||||||
if (hasBlockedIp(ip)) {
|
website: websiteId,
|
||||||
return forbidden();
|
hostname,
|
||||||
}
|
screen,
|
||||||
|
language,
|
||||||
|
url,
|
||||||
|
referrer,
|
||||||
|
name,
|
||||||
|
data,
|
||||||
|
title,
|
||||||
|
tag,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
const sessionId = uuid(websiteId, hostname, ip, userAgent);
|
// Cache check
|
||||||
|
let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null;
|
||||||
|
const cacheHeader = request.headers.get('x-umami-cache');
|
||||||
|
|
||||||
// Find session
|
if (cacheHeader) {
|
||||||
if (!cache?.sessionId) {
|
const result = await parseToken(cacheHeader, secret());
|
||||||
const session = await fetchSession(websiteId, sessionId);
|
|
||||||
|
|
||||||
// Create a session if not found
|
if (result) {
|
||||||
if (!session && !clickhouse.enabled) {
|
cache = result;
|
||||||
try {
|
}
|
||||||
await createSession({
|
}
|
||||||
id: sessionId,
|
|
||||||
websiteId,
|
// Find website
|
||||||
hostname,
|
if (!cache?.websiteId) {
|
||||||
browser,
|
const website = await fetchWebsite(websiteId);
|
||||||
os,
|
|
||||||
device,
|
if (!website) {
|
||||||
screen,
|
return badRequest('Website not found.');
|
||||||
language,
|
}
|
||||||
country,
|
}
|
||||||
subdivision1,
|
|
||||||
subdivision2,
|
// Client info
|
||||||
city,
|
const { ip, userAgent, device, browser, os, country, subdivision1, subdivision2, city } =
|
||||||
});
|
await getClientInfo(request, payload);
|
||||||
} catch (e: any) {
|
|
||||||
if (!e.message.toLowerCase().includes('unique constraint')) {
|
// IP block
|
||||||
return serverError(e);
|
if (hasBlockedIp(ip)) {
|
||||||
|
return forbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = uuid(websiteId, hostname, ip, userAgent);
|
||||||
|
|
||||||
|
// Find session
|
||||||
|
if (!clickhouse.enabled && !cache?.sessionId) {
|
||||||
|
const session = await fetchSession(websiteId, sessionId);
|
||||||
|
|
||||||
|
// Create a session if not found
|
||||||
|
if (!session) {
|
||||||
|
try {
|
||||||
|
await createSession({
|
||||||
|
id: sessionId,
|
||||||
|
websiteId,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
subdivision2,
|
||||||
|
city,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
if (!e.message.toLowerCase().includes('unique constraint')) {
|
||||||
|
return serverError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Visit info
|
// Visit info
|
||||||
const now = Math.floor(new Date().getTime() / 1000);
|
const now = Math.floor(new Date().getTime() / 1000);
|
||||||
let visitId = cache?.visitId || uuid(sessionId, visitSalt());
|
let visitId = cache?.visitId || uuid(sessionId, visitSalt());
|
||||||
let iat = cache?.iat || now;
|
let iat = cache?.iat || now;
|
||||||
|
|
||||||
// Expire visit after 30 minutes
|
// Expire visit after 30 minutes
|
||||||
if (now - iat > 1800) {
|
if (now - iat > 1800) {
|
||||||
visitId = uuid(sessionId, visitSalt());
|
visitId = uuid(sessionId, visitSalt());
|
||||||
iat = now;
|
iat = now;
|
||||||
}
|
|
||||||
|
|
||||||
if (type === COLLECTION_TYPE.event) {
|
|
||||||
const base = hostname ? `http://${hostname}` : 'http://localhost';
|
|
||||||
const currentUrl = new URL(url, base);
|
|
||||||
|
|
||||||
let urlPath = currentUrl.pathname;
|
|
||||||
const urlQuery = currentUrl.search.substring(1);
|
|
||||||
const urlDomain = currentUrl.hostname.replace(/^www\./, '');
|
|
||||||
|
|
||||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
|
||||||
urlPath = urlPath.replace(/(.+)\/$/, '$1');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let referrerPath: string;
|
if (type === COLLECTION_TYPE.event) {
|
||||||
let referrerQuery: string;
|
const base = hostname ? `https://${hostname}` : 'https://localhost';
|
||||||
let referrerDomain: string;
|
const currentUrl = new URL(url, base);
|
||||||
|
|
||||||
if (referrer) {
|
let urlPath = currentUrl.pathname;
|
||||||
const referrerUrl = new URL(referrer, base);
|
const urlQuery = currentUrl.search.substring(1);
|
||||||
|
const urlDomain = currentUrl.hostname.replace(/^www./, '');
|
||||||
|
|
||||||
referrerPath = referrerUrl.pathname;
|
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||||
referrerQuery = referrerUrl.search.substring(1);
|
urlPath = urlPath.replace(/(.+)\/$/, '$1');
|
||||||
|
|
||||||
if (referrerUrl.hostname !== 'localhost') {
|
|
||||||
referrerDomain = referrerUrl.hostname.replace(/^www\./, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let referrerPath: string;
|
||||||
|
let referrerQuery: string;
|
||||||
|
let referrerDomain: string;
|
||||||
|
|
||||||
|
if (referrer) {
|
||||||
|
const referrerUrl = new URL(referrer, base);
|
||||||
|
|
||||||
|
referrerPath = referrerUrl.pathname;
|
||||||
|
referrerQuery = referrerUrl.search.substring(1);
|
||||||
|
|
||||||
|
if (referrerUrl.hostname !== 'localhost') {
|
||||||
|
referrerDomain = referrerUrl.hostname.replace(/^www\./, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveEvent({
|
||||||
|
websiteId,
|
||||||
|
sessionId,
|
||||||
|
visitId,
|
||||||
|
urlPath,
|
||||||
|
urlQuery,
|
||||||
|
referrerPath,
|
||||||
|
referrerQuery,
|
||||||
|
referrerDomain,
|
||||||
|
pageTitle: title,
|
||||||
|
eventName: name,
|
||||||
|
eventData: data,
|
||||||
|
hostname: hostname || urlDomain,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
subdivision1,
|
||||||
|
subdivision2,
|
||||||
|
city,
|
||||||
|
tag,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveEvent({
|
if (type === COLLECTION_TYPE.identify) {
|
||||||
websiteId,
|
if (!data) {
|
||||||
sessionId,
|
return badRequest('Data required.');
|
||||||
visitId,
|
}
|
||||||
urlPath,
|
|
||||||
urlQuery,
|
|
||||||
referrerPath,
|
|
||||||
referrerQuery,
|
|
||||||
referrerDomain,
|
|
||||||
pageTitle: title,
|
|
||||||
eventName: name,
|
|
||||||
eventData: data,
|
|
||||||
hostname: hostname || urlDomain,
|
|
||||||
browser,
|
|
||||||
os,
|
|
||||||
device,
|
|
||||||
screen,
|
|
||||||
language,
|
|
||||||
country,
|
|
||||||
subdivision1,
|
|
||||||
subdivision2,
|
|
||||||
city,
|
|
||||||
tag,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === COLLECTION_TYPE.identify) {
|
await saveSessionData({
|
||||||
if (!data) {
|
websiteId,
|
||||||
return badRequest('Data required.');
|
sessionId,
|
||||||
|
sessionData: data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveSessionData({
|
const token = createToken({ websiteId, sessionId, visitId, iat }, secret());
|
||||||
websiteId,
|
|
||||||
sessionId,
|
return json({ cache: token });
|
||||||
sessionData: data,
|
} catch (e) {
|
||||||
});
|
return serverError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = createToken({ websiteId, sessionId, visitId, iat }, secret());
|
|
||||||
|
|
||||||
return json({ cache: token });
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { Report } from '@prisma/client';
|
import { Report } from '@prisma/client';
|
||||||
import { getClient, redisEnabled } from '@umami/redis-client';
|
import redis from '@/lib/redis';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants';
|
import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants';
|
||||||
import { secret, getRandomChars } from '@/lib/crypto';
|
import { secret, getRandomChars } from '@/lib/crypto';
|
||||||
|
|
@ -31,10 +31,8 @@ export async function checkAuth(request: Request) {
|
||||||
|
|
||||||
if (userId) {
|
if (userId) {
|
||||||
user = await getUser(userId);
|
user = await getUser(userId);
|
||||||
} else if (redisEnabled && authKey) {
|
} else if (redis.enabled && authKey) {
|
||||||
const redis = getClient();
|
const key = await redis.client.get(authKey);
|
||||||
|
|
||||||
const key = await redis.get(authKey);
|
|
||||||
|
|
||||||
if (key?.userId) {
|
if (key?.userId) {
|
||||||
user = await getUser(key.userId);
|
user = await getUser(key.userId);
|
||||||
|
|
@ -66,12 +64,12 @@ export async function checkAuth(request: Request) {
|
||||||
export async function saveAuth(data: any, expire = 0) {
|
export async function saveAuth(data: any, expire = 0) {
|
||||||
const authKey = `auth:${getRandomChars(32)}`;
|
const authKey = `auth:${getRandomChars(32)}`;
|
||||||
|
|
||||||
const redis = getClient();
|
if (redis.enabled) {
|
||||||
|
await redis.client.set(authKey, data);
|
||||||
|
|
||||||
await redis.set(authKey, data);
|
if (expire) {
|
||||||
|
await redis.client.expire(authKey, expire);
|
||||||
if (expire) {
|
}
|
||||||
await redis.expire(authKey, expire);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createSecureToken({ authKey }, secret());
|
return createSecureToken({ authKey }, secret());
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import { Website, Session } from '@prisma/client';
|
import { Website, Session } from '@prisma/client';
|
||||||
import { getClient, redisEnabled } from '@umami/redis-client';
|
import redis from '@/lib/redis';
|
||||||
import { getWebsiteSession, getWebsite } from '@/queries';
|
import { getWebsiteSession, getWebsite } from '@/queries';
|
||||||
|
|
||||||
export async function fetchWebsite(websiteId: string): Promise<Website> {
|
export async function fetchWebsite(websiteId: string): Promise<Website> {
|
||||||
let website = null;
|
let website = null;
|
||||||
|
|
||||||
if (redisEnabled) {
|
if (redis.enabled) {
|
||||||
const redis = getClient();
|
website = await redis.client.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
|
||||||
|
|
||||||
website = await redis.fetch(`website:${websiteId}`, () => getWebsite(websiteId), 86400);
|
|
||||||
} else {
|
} else {
|
||||||
website = await getWebsite(websiteId);
|
website = await getWebsite(websiteId);
|
||||||
}
|
}
|
||||||
|
|
@ -23,10 +21,8 @@ export async function fetchWebsite(websiteId: string): Promise<Website> {
|
||||||
export async function fetchSession(websiteId: string, sessionId: string): Promise<Session> {
|
export async function fetchSession(websiteId: string, sessionId: string): Promise<Session> {
|
||||||
let session = null;
|
let session = null;
|
||||||
|
|
||||||
if (redisEnabled) {
|
if (redis.enabled) {
|
||||||
const redis = getClient();
|
session = await redis.client.fetch(
|
||||||
|
|
||||||
session = await redis.fetch(
|
|
||||||
`session:${sessionId}`,
|
`session:${sessionId}`,
|
||||||
() => getWebsiteSession(websiteId, sessionId),
|
() => getWebsiteSession(websiteId, sessionId),
|
||||||
86400,
|
86400,
|
||||||
|
|
|
||||||
17
src/lib/redis.ts
Normal file
17
src/lib/redis.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { REDIS, UmamiRedisClient } from '@umami/redis-client';
|
||||||
|
|
||||||
|
const enabled = !!process.env.REDIS_URL;
|
||||||
|
|
||||||
|
function getClient() {
|
||||||
|
const client = new UmamiRedisClient(process.env.REDIS_URL);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
global[REDIS] = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = global[REDIS] || getClient();
|
||||||
|
|
||||||
|
export default { client, enabled };
|
||||||
|
|
@ -7,7 +7,7 @@ import { getWebsiteDateRange } from '@/queries';
|
||||||
|
|
||||||
export async function getJsonBody(request: Request) {
|
export async function getJsonBody(request: Request) {
|
||||||
try {
|
try {
|
||||||
return request.clone().json();
|
return await request.clone().json();
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,20 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value),
|
||||||
|
|
||||||
export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']);
|
export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']);
|
||||||
|
|
||||||
|
export const urlOrPathParam = z.string().refine(
|
||||||
|
value => {
|
||||||
|
try {
|
||||||
|
new URL(value, 'https://localhost');
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Invalid URL.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const reportTypeParam = z.enum([
|
export const reportTypeParam = z.enum([
|
||||||
'funnel',
|
'funnel',
|
||||||
'insights',
|
'insights',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Prisma, Website } from '@prisma/client';
|
import { Prisma, Website } from '@prisma/client';
|
||||||
import { getClient } from '@umami/redis-client';
|
import redis from '@/lib/redis';
|
||||||
import prisma from '@/lib/prisma';
|
import prisma from '@/lib/prisma';
|
||||||
import { PageResult, PageParams } from '@/lib/types';
|
import { PageResult, PageParams } from '@/lib/types';
|
||||||
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
|
import WebsiteFindManyArgs = Prisma.WebsiteFindManyArgs;
|
||||||
|
|
@ -182,9 +182,7 @@ export async function resetWebsite(
|
||||||
}),
|
}),
|
||||||
]).then(async data => {
|
]).then(async data => {
|
||||||
if (cloudMode) {
|
if (cloudMode) {
|
||||||
const redis = getClient();
|
await redis.client.set(`website:${websiteId}`, data[3]);
|
||||||
|
|
||||||
await redis.set(`website:${websiteId}`, data[3]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -227,9 +225,7 @@ export async function deleteWebsite(
|
||||||
}),
|
}),
|
||||||
]).then(async data => {
|
]).then(async data => {
|
||||||
if (cloudMode) {
|
if (cloudMode) {
|
||||||
const redis = getClient();
|
await redis.client.del(`website:${websiteId}`);
|
||||||
|
|
||||||
await redis.del(`website:${websiteId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
|
|
@ -3402,10 +3402,10 @@
|
||||||
chalk "^4.1.2"
|
chalk "^4.1.2"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
|
|
||||||
"@umami/redis-client@^0.24.0":
|
"@umami/redis-client@^0.25.0":
|
||||||
version "0.24.0"
|
version "0.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@umami/redis-client/-/redis-client-0.24.0.tgz#8af489250396be76bc0906766343620589774c4b"
|
resolved "https://registry.yarnpkg.com/@umami/redis-client/-/redis-client-0.25.0.tgz#8bf01f22ceb3b90e15e59ab8daf44f838b83a6a7"
|
||||||
integrity sha512-yUZmC87H5QZKNA6jD9k/7d8WDaXQTDROlpyK7S+V2csD96eAnMNi7JsWAVWx9T/584QKD8DsSIy87PTWq1HNPw==
|
integrity sha512-j2GUehtrUfNPuikmcVXucgnL04gQOtbLiG20NqdlUXlDA/ebkV/waDfcYtMLuvXOFwiEeTatqPFEfXYuLDwJWw==
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
redis "^4.5.1"
|
redis "^4.5.1"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue