From 0aaba8cbd1f74664e9b8e0ab52aae326ee146cc3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 15 Nov 2022 13:21:14 -0800 Subject: [PATCH 01/38] Initial Typescript models. --- db/clickhouse/schema.sql | 3 + db/postgresql/schema.prisma | 5 +- interface/api/auth.d.ts | 8 ++ interface/api/models.d.ts | 73 ++++++++++++ interface/api/nextApi.d.ts | 14 +++ interface/auth.d.ts | 5 - interface/base.d.ts | 22 ---- interface/enum.d.ts | 7 ++ interface/index.d.ts | 22 ---- lib/clickhouse.js | 8 +- lib/{prisma.js => prisma.ts} | 74 +++++------- pages/api/auth/{login.js => login.ts} | 20 +++- pages/api/auth/{logout.js => logout.ts} | 3 +- pages/api/auth/verify.js | 8 -- pages/api/auth/verify.ts | 10 ++ pages/api/{collect.js => collect.ts} | 18 ++- pages/api/{config.js => config.ts} | 11 +- pages/api/heartbeat.js | 5 - pages/api/heartbeat.ts | 6 + pages/api/realtime/{init.js => init.ts} | 11 +- pages/api/realtime/{update.js => update.ts} | 12 +- pages/api/share/{[id].js => [id].ts} | 20 +++- pages/api/users/[id]/{index.js => index.ts} | 19 +++- .../users/[id]/{password.js => password.ts} | 17 ++- pages/api/users/{index.js => index.ts} | 14 ++- .../websites/[id]/{active.js => active.ts} | 12 +- .../[id]/{eventdata.js => eventdata.ts} | 21 +++- .../websites/[id]/{events.js => events.ts} | 38 +++++-- .../api/websites/[id]/{index.js => index.ts} | 18 ++- .../websites/[id]/{metrics.js => metrics.ts} | 21 +++- .../[id]/{pageviews.js => pageviews.ts} | 39 +++++-- .../api/websites/[id]/{reset.js => reset.ts} | 11 +- .../api/websites/[id]/{stats.js => stats.ts} | 21 +++- pages/api/websites/{index.js => index.ts} | 17 ++- queries/admin/user/createUser.js | 7 -- queries/admin/user/createUser.ts | 20 ++++ .../user/{deleteUser.js => deleteUser.ts} | 5 +- queries/admin/user/getUser.js | 7 -- queries/admin/user/getUser.ts | 25 +++++ .../admin/user/{getUsers.js => getUsers.ts} | 3 +- queries/admin/user/updateUser.js | 8 -- queries/admin/user/updateUser.ts | 19 ++++ .../{createWebsite.js => createWebsite.ts} | 13 ++- .../{deleteWebsite.js => deleteWebsite.ts} | 13 ++- queries/admin/website/getAllWebsites.js | 23 ---- queries/admin/website/getAllWebsites.ts | 24 ++++ ...{getUserWebsites.js => getUserWebsites.ts} | 3 +- queries/admin/website/getWebsite.js | 7 -- queries/admin/website/getWebsite.ts | 8 ++ .../{resetWebsite.js => resetWebsite.ts} | 15 ++- queries/admin/website/updateWebsite.js | 10 -- queries/admin/website/updateWebsite.ts | 11 ++ .../{getEventData.js => getEventData.ts} | 55 +++++++-- queries/analytics/event/getEventMetrics.js | 68 ------------ queries/analytics/event/getEventMetrics.ts | 105 ++++++++++++++++++ queries/analytics/event/getEvents.js | 45 -------- queries/analytics/event/saveEvent.js | 62 ----------- queries/analytics/event/saveEvent.ts | 91 +++++++++++++++ .../analytics/pageview/getPageviewMetrics.js | 59 ---------- .../analytics/pageview/getPageviewMetrics.ts | 82 ++++++++++++++ .../analytics/pageview/getPageviewParams.js | 41 ------- .../analytics/pageview/getPageviewStats.js | 78 ------------- .../analytics/pageview/getPageviewStats.ts | 102 +++++++++++++++++ queries/analytics/pageview/getPageviews.js | 43 ------- .../{savePageView.js => savePageView.ts} | 32 +++++- .../{createSession.js => createSession.ts} | 21 +++- .../session/{getSession.js => getSession.ts} | 11 +- ...SessionMetrics.js => getSessionMetrics.ts} | 29 +++-- queries/analytics/session/getSessions.js | 52 --------- ...ActiveVisitors.js => getActiveVisitors.ts} | 6 +- queries/analytics/stats/getRealtimeData.js | 30 ----- queries/analytics/stats/getRealtimeData.ts | 3 + ...{getWebsiteStats.js => getWebsiteStats.ts} | 35 +++--- tsconfig.json | 28 +++++ 74 files changed, 1144 insertions(+), 768 deletions(-) create mode 100644 interface/api/auth.d.ts create mode 100644 interface/api/models.d.ts create mode 100644 interface/api/nextApi.d.ts delete mode 100644 interface/auth.d.ts delete mode 100644 interface/base.d.ts create mode 100644 interface/enum.d.ts rename lib/{prisma.js => prisma.ts} (70%) rename pages/api/auth/{login.js => login.ts} (65%) rename pages/api/auth/{logout.js => logout.ts} (74%) delete mode 100644 pages/api/auth/verify.js create mode 100644 pages/api/auth/verify.ts rename pages/api/{collect.js => collect.ts} (83%) rename pages/api/{config.js => config.ts} (57%) delete mode 100644 pages/api/heartbeat.js create mode 100644 pages/api/heartbeat.ts rename pages/api/realtime/{init.js => init.ts} (60%) rename pages/api/realtime/{update.js => update.ts} (64%) rename pages/api/share/{[id].js => [id].ts} (52%) rename pages/api/users/[id]/{index.js => index.ts} (77%) rename pages/api/users/[id]/{password.js => password.ts} (65%) rename pages/api/users/{index.js => index.ts} (70%) rename pages/api/websites/[id]/{active.js => active.ts} (62%) rename pages/api/websites/[id]/{eventdata.js => eventdata.ts} (61%) rename pages/api/websites/[id]/{events.js => events.ts} (53%) rename pages/api/websites/[id]/{index.js => index.ts} (72%) rename pages/api/websites/[id]/{metrics.js => metrics.ts} (85%) rename pages/api/websites/[id]/{pageviews.js => pageviews.ts} (67%) rename pages/api/websites/[id]/{reset.js => reset.ts} (66%) rename pages/api/websites/[id]/{stats.js => stats.ts} (76%) rename pages/api/websites/{index.js => index.ts} (68%) delete mode 100644 queries/admin/user/createUser.js create mode 100644 queries/admin/user/createUser.ts rename queries/admin/user/{deleteUser.js => deleteUser.ts} (83%) delete mode 100644 queries/admin/user/getUser.js create mode 100644 queries/admin/user/getUser.ts rename queries/admin/user/{getUsers.js => getUsers.ts} (76%) delete mode 100644 queries/admin/user/updateUser.js create mode 100644 queries/admin/user/updateUser.ts rename queries/admin/website/{createWebsite.js => createWebsite.ts} (63%) rename queries/admin/website/{deleteWebsite.js => deleteWebsite.ts} (52%) delete mode 100644 queries/admin/website/getAllWebsites.js create mode 100644 queries/admin/website/getAllWebsites.ts rename queries/admin/website/{getUserWebsites.js => getUserWebsites.ts} (59%) delete mode 100644 queries/admin/website/getWebsite.js create mode 100644 queries/admin/website/getWebsite.ts rename queries/admin/website/{resetWebsite.js => resetWebsite.ts} (51%) delete mode 100644 queries/admin/website/updateWebsite.js create mode 100644 queries/admin/website/updateWebsite.ts rename queries/analytics/event/{getEventData.js => getEventData.ts} (54%) delete mode 100644 queries/analytics/event/getEventMetrics.js create mode 100644 queries/analytics/event/getEventMetrics.ts delete mode 100644 queries/analytics/event/getEvents.js delete mode 100644 queries/analytics/event/saveEvent.js create mode 100644 queries/analytics/event/saveEvent.ts delete mode 100644 queries/analytics/pageview/getPageviewMetrics.js create mode 100644 queries/analytics/pageview/getPageviewMetrics.ts delete mode 100644 queries/analytics/pageview/getPageviewParams.js delete mode 100644 queries/analytics/pageview/getPageviewStats.js create mode 100644 queries/analytics/pageview/getPageviewStats.ts delete mode 100644 queries/analytics/pageview/getPageviews.js rename queries/analytics/pageview/{savePageView.js => savePageView.ts} (60%) rename queries/analytics/session/{createSession.js => createSession.ts} (63%) rename queries/analytics/session/{getSession.js => getSession.ts} (64%) rename queries/analytics/session/{getSessionMetrics.js => getSessionMetrics.ts} (63%) delete mode 100644 queries/analytics/session/getSessions.js rename queries/analytics/stats/{getActiveVisitors.js => getActiveVisitors.ts} (83%) delete mode 100644 queries/analytics/stats/getRealtimeData.js create mode 100644 queries/analytics/stats/getRealtimeData.ts rename queries/analytics/stats/{getWebsiteStats.js => getWebsiteStats.ts} (70%) create mode 100644 tsconfig.json diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 0579d13d..64d31c04 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -19,6 +19,7 @@ CREATE TABLE event url String, referrer String, --event + event_type UInt32, event_name String, event_data JSON, created_at DateTime('UTC') @@ -41,6 +42,7 @@ CREATE TABLE event_queue ( screen LowCardinality(String), language LowCardinality(String), country LowCardinality(String), + event_type UInt32, event_name String, event_data String, created_at DateTime('UTC') @@ -67,6 +69,7 @@ SELECT website_id, screen, language, country, + event_type, event_name, event_data, created_at diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 58b28a8b..b2cb4662 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -69,8 +69,9 @@ model WebsiteEvent { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) url String @db.VarChar(500) referrer String? @db.VarChar(500) - eventName String @map("event_name") @db.VarChar(50) - eventData Json @map("event_data") + eventType Int @default(1) @map("event_type") @db.Integer + eventName String? @map("event_name") @db.VarChar(50) + eventData Json? @map("event_data") @@index([createdAt]) @@index([sessionId]) diff --git a/interface/api/auth.d.ts b/interface/api/auth.d.ts new file mode 100644 index 00000000..6e4e3494 --- /dev/null +++ b/interface/api/auth.d.ts @@ -0,0 +1,8 @@ +export interface Auth { + user: { + id: string; + username: string; + isAdmin: boolean; + }; + shareToken: string; +} diff --git a/interface/api/models.d.ts b/interface/api/models.d.ts new file mode 100644 index 00000000..6251a149 --- /dev/null +++ b/interface/api/models.d.ts @@ -0,0 +1,73 @@ +export interface User { + id: string; + username: string; + isAdmin: boolean; + createdAt: string; +} + +export interface Website { + id: string; + userId: string; + revId: number; + name: string; + domain: string; + shareId: string; + createdAt: Date; +} + +export interface Share { + id: string; + token: string; +} + +export interface Empty {} + +export interface WebsiteActive { + x: number; +} + +export interface WebsiteEventDataMetric { + [key: string]: number; +} + +export interface WebsiteMetric { + x: string; + y: number; +} + +export interface WebsiteEventMetric { + x: string; + t: string; + y: number; +} + +export interface WebsitePageviews { + pageviews: { + t: string; + y: number; + }; + sessions: { + t: string; + y: number; + }; +} + +export interface WebsiteStats { + pageviews: { value: number; change: number }; + uniques: { value: number; change: number }; + bounces: { value: number; change: number }; + totalTime: { value: number; change: number }; +} + +export interface RealtimeInit { + websites: Website[]; + token: string; + data: RealtimeUpdate; +} + +export interface RealtimeUpdate { + pageviews: any[]; + sessions: any[]; + events: any[]; + timestamp: number; +} diff --git a/interface/api/nextApi.d.ts b/interface/api/nextApi.d.ts new file mode 100644 index 00000000..c5a54515 --- /dev/null +++ b/interface/api/nextApi.d.ts @@ -0,0 +1,14 @@ +import { NextApiRequest } from 'next'; +import { Auth } from './auth'; + +export interface NextApiRequestQueryBody extends NextApiRequest { + auth?: Auth; + query: TQuery & { [key: string]: string | string[] }; + body: TBody; + headers: any; +} + +export interface NextApiRequestAuth extends NextApiRequest { + auth?: Auth; + headers: any; +} diff --git a/interface/auth.d.ts b/interface/auth.d.ts deleted file mode 100644 index 36d3041e..00000000 --- a/interface/auth.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Auth { - id: number; - email?: string; - teams?: string[]; -} diff --git a/interface/base.d.ts b/interface/base.d.ts deleted file mode 100644 index 5d498ebf..00000000 --- a/interface/base.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextApiRequest } from 'next'; -import { Auth } from './auth'; - -export interface NextApiRequestQueryBody extends NextApiRequest { - auth: Auth; - query: TQuery; - body: TBody; -} - -export interface NextApiRequestQuery extends NextApiRequest { - auth: Auth; - query: TQuery; -} - -export interface NextApiRequestBody extends NextApiRequest { - auth: Auth; - body: TBody; -} - -export interface ObjectAny { - [key: string]: any; -} diff --git a/interface/enum.d.ts b/interface/enum.d.ts new file mode 100644 index 00000000..8b09039c --- /dev/null +++ b/interface/enum.d.ts @@ -0,0 +1,7 @@ +/* eslint-disable no-unused-vars */ +export namespace UmamiApi { + enum EventType { + Pageview = 1, + Event = 2, + } +} diff --git a/interface/index.d.ts b/interface/index.d.ts index 5d498ebf..e69de29b 100644 --- a/interface/index.d.ts +++ b/interface/index.d.ts @@ -1,22 +0,0 @@ -import { NextApiRequest } from 'next'; -import { Auth } from './auth'; - -export interface NextApiRequestQueryBody extends NextApiRequest { - auth: Auth; - query: TQuery; - body: TBody; -} - -export interface NextApiRequestQuery extends NextApiRequest { - auth: Auth; - query: TQuery; -} - -export interface NextApiRequestBody extends NextApiRequest { - auth: Auth; - body: TBody; -} - -export interface ObjectAny { - [key: string]: any; -} diff --git a/lib/clickhouse.js b/lib/clickhouse.js index 3d44daac..b28694b6 100644 --- a/lib/clickhouse.js +++ b/lib/clickhouse.js @@ -106,7 +106,7 @@ function getEventDataFilterQuery(column, filters) { return query.join('\nand '); } -function getFilterQuery(column, filters = {}, params = []) { +function getFilterQuery(filters = {}, params = []) { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -146,7 +146,7 @@ function getFilterQuery(column, filters = {}, params = []) { return query.join('\n'); } -function parseFilters(column, filters = {}, params = []) { +function parseFilters(filters = {}, params = []) { const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } = filters; @@ -159,9 +159,7 @@ function parseFilters(column, filters = {}, params = []) { sessionFilters, eventFilters, event: { event_name }, - pageviewQuery: getFilterQuery(column, pageviewFilters, params), - sessionQuery: getFilterQuery(column, sessionFilters, params), - eventQuery: getFilterQuery(column, eventFilters, params), + filterQuery: getFilterQuery(filters, params), }; } diff --git a/lib/prisma.js b/lib/prisma.ts similarity index 70% rename from lib/prisma.js rename to lib/prisma.ts index ab1e6ebf..08cfb0e0 100644 --- a/lib/prisma.js +++ b/lib/prisma.ts @@ -4,6 +4,7 @@ import moment from 'moment-timezone'; import debug from 'debug'; import { PRISMA, MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db'; import { FILTER_IGNORED } from 'lib/constants'; +import { PrismaClientOptions } from '@prisma/client/runtime'; const MYSQL_DATE_FORMATS = { minute: '%Y-%m-%d %H:%i:00', @@ -37,7 +38,8 @@ function logQuery(e) { } function getClient(options) { - const prisma = new PrismaClient(options); + const prisma: PrismaClient = + new PrismaClient(options); if (process.env.LOG_QUERY) { prisma.$on('query', logQuery); @@ -52,7 +54,7 @@ function getClient(options) { return prisma; } -function getDateQuery(field, unit, timezone) { +function getDateQuery(field, unit, timezone?): string { const db = getDatabaseType(process.env.DATABASE_URL); if (db === POSTGRESQL) { @@ -73,7 +75,7 @@ function getDateQuery(field, unit, timezone) { } } -function getTimestampInterval(field) { +function getTimestampInterval(field): string { const db = getDatabaseType(process.env.DATABASE_URL); if (db === POSTGRESQL) { @@ -85,7 +87,7 @@ function getTimestampInterval(field) { } } -function getJsonField(column, property, isNumber) { +function getJsonField(column, property, isNumber): string { const db = getDatabaseType(process.env.DATABASE_URL); if (db === POSTGRESQL) { @@ -103,7 +105,7 @@ function getJsonField(column, property, isNumber) { } } -function getEventDataColumnsQuery(column, columns) { +function getEventDataColumnsQuery(column, columns): string { const query = Object.keys(columns).reduce((arr, key) => { const filter = columns[key]; @@ -121,7 +123,7 @@ function getEventDataColumnsQuery(column, columns) { return query.join(',\n'); } -function getEventDataFilterQuery(column, filters) { +function getEventDataFilterQuery(column, filters): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -143,7 +145,7 @@ function getEventDataFilterQuery(column, filters) { return query.join('\nand '); } -function getFilterQuery(table, column, filters = {}, params = []) { +function getFilterQuery(filters = {}, params = []): string { const query = Object.keys(filters).reduce((arr, key) => { const filter = filters[key]; @@ -153,48 +155,25 @@ function getFilterQuery(table, column, filters = {}, params = []) { switch (key) { case 'url': - if (table === 'pageview' || table === 'event') { - arr.push(`and ${table}.${key}=$${params.length + 1}`); - params.push(decodeURIComponent(filter)); - } - break; - case 'os': case 'browser': case 'device': case 'country': - if (table === 'session') { - arr.push(`and ${table}.${key}=$${params.length + 1}`); - params.push(decodeURIComponent(filter)); - } - break; - case 'event_name': - if (table === 'event') { - arr.push(`and ${table}.${key}=$${params.length + 1}`); - params.push(decodeURIComponent(filter)); - } + arr.push(`and ${key}=$${params.length + 1}`); + params.push(decodeURIComponent(filter)); break; - case 'referrer': - if (table === 'pageview' || table === 'event') { - arr.push(`and ${table}.referrer like $${params.length + 1}`); - params.push(`%${decodeURIComponent(filter)}%`); - } + arr.push(`and referrer like $${params.length + 1}`); + params.push(`%${decodeURIComponent(filter)}%`); break; - case 'domain': - if (table === 'pageview') { - arr.push(`and ${table}.referrer not like $${params.length + 1}`); - arr.push(`and ${table}.referrer not like '/%'`); - params.push(`%://${filter}/%`); - } + arr.push(`and referrer not like $${params.length + 1}`); + arr.push(`and referrer not like '/%'`); + params.push(`%://${filter}/%`); break; - case 'query': - if (table === 'pageview') { - arr.push(`and ${table}.url like '%?%'`); - } + arr.push(`and url like '%?%'`); } return arr; @@ -203,7 +182,11 @@ function getFilterQuery(table, column, filters = {}, params = []) { return query.join('\n'); } -function parseFilters(table, column, filters = {}, params = [], sessionKey = 'session_id') { +function parseFilters( + filters: { [key: string]: any } = {}, + params = [], + sessionKey = 'session_id', +) { const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } = filters; @@ -218,15 +201,13 @@ function parseFilters(table, column, filters = {}, params = [], sessionKey = 'se event: { event_name }, joinSession: os || browser || device || country - ? `inner join session on ${table}.${sessionKey} = session.${sessionKey}` + ? `inner join session on ${sessionKey} = session.${sessionKey}` : '', - pageviewQuery: getFilterQuery('pageview', column, pageviewFilters, params), - sessionQuery: getFilterQuery('session', column, sessionFilters, params), - eventQuery: getFilterQuery('event', column, eventFilters, params), + filterQuery: getFilterQuery(filters, params), }; } -async function rawQuery(query, params = []) { +async function rawQuery(query, params = []): Promise { const db = getDatabaseType(process.env.DATABASE_URL); if (db !== POSTGRESQL && db !== MYSQL) { @@ -238,12 +219,13 @@ async function rawQuery(query, params = []) { return prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]); } -async function transaction(queries) { +async function transaction(queries): Promise { return prisma.$transaction(queries); } // Initialization -const prisma = global[PRISMA] || getClient(PRISMA_OPTIONS); +const prisma: PrismaClient = + global[PRISMA] || getClient(PRISMA_OPTIONS); export default { client: prisma, diff --git a/pages/api/auth/login.js b/pages/api/auth/login.ts similarity index 65% rename from pages/api/auth/login.js rename to pages/api/auth/login.ts index a54e8013..321fb3ab 100644 --- a/pages/api/auth/login.js +++ b/pages/api/auth/login.ts @@ -10,8 +10,24 @@ import { import { getUser } from 'queries'; import { secret } from 'lib/crypto'; import redis from 'lib/redis'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { User } from 'interface/api/models'; -export default async (req, res) => { +export interface LoginRequestBody { + username: string; + password: string; +} + +export interface LoginResponse { + token: string; + user: User; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { if (req.method === 'POST') { const { username, password } = req.body; @@ -19,7 +35,7 @@ export default async (req, res) => { return badRequest(res); } - const user = await getUser({ username }); + const user = await getUser({ username }, true); if (user && checkPassword(password, user.password)) { if (redis.enabled) { diff --git a/pages/api/auth/logout.js b/pages/api/auth/logout.ts similarity index 74% rename from pages/api/auth/logout.js rename to pages/api/auth/logout.ts index 37f117be..c05a05de 100644 --- a/pages/api/auth/logout.js +++ b/pages/api/auth/logout.ts @@ -2,8 +2,9 @@ import { methodNotAllowed, ok } from 'next-basics'; import { useAuth } from 'lib/middleware'; import redis from 'lib/redis'; import { getAuthToken } from 'lib/auth'; +import { NextApiRequest, NextApiResponse } from 'next'; -export default async (req, res) => { +export default async (req: NextApiRequest, res: NextApiResponse) => { await useAuth(req, res); if (req.method === 'POST') { diff --git a/pages/api/auth/verify.js b/pages/api/auth/verify.js deleted file mode 100644 index 670480b1..00000000 --- a/pages/api/auth/verify.js +++ /dev/null @@ -1,8 +0,0 @@ -import { useAuth } from 'lib/middleware'; -import { ok } from 'next-basics'; - -export default async (req, res) => { - await useAuth(req, res); - - return ok(res, req.auth); -}; diff --git a/pages/api/auth/verify.ts b/pages/api/auth/verify.ts new file mode 100644 index 00000000..024cd96a --- /dev/null +++ b/pages/api/auth/verify.ts @@ -0,0 +1,10 @@ +import { NextApiRequestAuth } from 'interface/api/nextApi'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { ok } from 'next-basics'; + +export default async (req: NextApiRequestAuth, res: NextApiResponse) => { + await useAuth(req, res); + + return ok(res, req.auth); +}; diff --git a/pages/api/collect.js b/pages/api/collect.ts similarity index 83% rename from pages/api/collect.js rename to pages/api/collect.ts index 87e516cc..2aa2bde1 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.ts @@ -6,8 +6,23 @@ import { savePageView, saveEvent } from 'queries'; import { useCors, useSession } from 'lib/middleware'; import { getJsonBody, getIpAddress } from 'lib/request'; import { secret } from 'lib/crypto'; +import { NextApiRequest, NextApiResponse } from 'next'; -export default async (req, res) => { +export interface NextApiRequestCollect extends NextApiRequest { + session: { + id: string; + websiteId: string; + hostname: string; + browser: string; + os: string; + device: string; + screen: string; + language: string; + country: string; + }; +} + +export default async (req: NextApiRequestCollect, res: NextApiResponse) => { await useCors(req, res); if (isbot(req.headers['user-agent']) && !process.env.DISABLE_BOT_CHECK) { @@ -74,6 +89,7 @@ export default async (req, res) => { await saveEvent({ ...session, url, + referrer, eventName, eventData, }); diff --git a/pages/api/config.js b/pages/api/config.ts similarity index 57% rename from pages/api/config.js rename to pages/api/config.ts index faf94c0b..e5ae318a 100644 --- a/pages/api/config.js +++ b/pages/api/config.ts @@ -1,6 +1,15 @@ +import { NextApiRequest, NextApiResponse } from 'next'; import { ok, methodNotAllowed } from 'next-basics'; -export default async (req, res) => { +export interface ConfigResponse { + basePath: string; + trackerScriptName: string; + updatesDisabled: boolean; + telemetryDisabled: boolean; + adminDisabled: boolean; +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'GET') { return ok(res, { basePath: process.env.BASE_PATH || '', diff --git a/pages/api/heartbeat.js b/pages/api/heartbeat.js deleted file mode 100644 index a4ee5923..00000000 --- a/pages/api/heartbeat.js +++ /dev/null @@ -1,5 +0,0 @@ -import { ok } from 'next-basics'; - -export default async (req, res) => { - return ok(res); -}; diff --git a/pages/api/heartbeat.ts b/pages/api/heartbeat.ts new file mode 100644 index 00000000..1b515d39 --- /dev/null +++ b/pages/api/heartbeat.ts @@ -0,0 +1,6 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { ok } from 'next-basics'; + +export default async (req: NextApiRequest, res: NextApiResponse) => { + return ok(res); +}; diff --git a/pages/api/realtime/init.js b/pages/api/realtime/init.ts similarity index 60% rename from pages/api/realtime/init.js rename to pages/api/realtime/init.ts index 16e7cad3..b1d1f32f 100644 --- a/pages/api/realtime/init.js +++ b/pages/api/realtime/init.ts @@ -1,10 +1,13 @@ import { subMinutes } from 'date-fns'; -import { ok, methodNotAllowed, createToken } from 'next-basics'; -import { useAuth } from 'lib/middleware'; -import { getUserWebsites, getRealtimeData } from 'queries'; +import { RealtimeInit } from 'interface/api/models'; +import { NextApiRequestAuth } from 'interface/api/nextApi'; import { secret } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { createToken, methodNotAllowed, ok } from 'next-basics'; +import { getRealtimeData, getUserWebsites } from 'queries'; -export default async (req, res) => { +export default async (req: NextApiRequestAuth, res: NextApiResponse) => { await useAuth(req, res); if (req.method === 'GET') { diff --git a/pages/api/realtime/update.js b/pages/api/realtime/update.ts similarity index 64% rename from pages/api/realtime/update.js rename to pages/api/realtime/update.ts index 9b91663d..239ab1e6 100644 --- a/pages/api/realtime/update.js +++ b/pages/api/realtime/update.ts @@ -3,8 +3,18 @@ import { useAuth } from 'lib/middleware'; import { getRealtimeData } from 'queries'; import { SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { RealtimeUpdate } from 'interface/api/models'; -export default async (req, res) => { +export interface InitUpdateRequestQuery { + start_at: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); if (req.method === 'GET') { diff --git a/pages/api/share/[id].js b/pages/api/share/[id].ts similarity index 52% rename from pages/api/share/[id].js rename to pages/api/share/[id].ts index 6fb19739..51385bee 100644 --- a/pages/api/share/[id].js +++ b/pages/api/share/[id].ts @@ -1,8 +1,22 @@ -import { getWebsite } from 'queries'; -import { ok, notFound, methodNotAllowed, createToken } from 'next-basics'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { secret } from 'lib/crypto'; +import { NextApiResponse } from 'next'; +import { createToken, methodNotAllowed, notFound, ok } from 'next-basics'; +import { getWebsite } from 'queries'; -export default async (req, res) => { +export interface ShareRequestQuery { + id: string; +} + +export interface ShareResponse { + id: string; + token: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { const { id: shareId } = req.query; if (req.method === 'GET') { diff --git a/pages/api/users/[id]/index.js b/pages/api/users/[id]/index.ts similarity index 77% rename from pages/api/users/[id]/index.js rename to pages/api/users/[id]/index.ts index a373bbd1..870ff68a 100644 --- a/pages/api/users/[id]/index.js +++ b/pages/api/users/[id]/index.ts @@ -1,8 +1,23 @@ import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getUser, deleteUser, updateUser } from 'queries'; import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { User } from 'interface/api/models'; -export default async (req, res) => { +export interface UserReqeustQuery { + id: string; +} + +export interface UserReqeustBody { + username: string; + password: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); const { @@ -29,7 +44,7 @@ export default async (req, res) => { const user = await getUser({ id }); - const data = {}; + const data: any = {}; if (password) { data.password = hashPassword(password); diff --git a/pages/api/users/[id]/password.js b/pages/api/users/[id]/password.ts similarity index 65% rename from pages/api/users/[id]/password.js rename to pages/api/users/[id]/password.ts index 6cad82ed..4b00d7d5 100644 --- a/pages/api/users/[id]/password.js +++ b/pages/api/users/[id]/password.ts @@ -10,8 +10,23 @@ import { } from 'next-basics'; import { allowQuery } from 'lib/auth'; import { TYPE_USER } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { User } from 'interface/api/models'; -export default async (req, res) => { +export interface UserPasswordRequestQuery { + id: string; +} + +export interface UserPasswordRequestBody { + current_password: string; + new_password: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); const { current_password, new_password } = req.body; diff --git a/pages/api/users/index.js b/pages/api/users/index.ts similarity index 70% rename from pages/api/users/index.js rename to pages/api/users/index.ts index f4a5010a..6b942287 100644 --- a/pages/api/users/index.js +++ b/pages/api/users/index.ts @@ -2,8 +2,20 @@ import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'ne import { useAuth } from 'lib/middleware'; import { uuid } from 'lib/crypto'; import { createUser, getUser, getUsers } from 'queries'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { User } from 'interface/api/models'; -export default async (req, res) => { +export interface UsersRequestBody { + username: string; + password: string; + id: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useAuth(req, res); const { diff --git a/pages/api/websites/[id]/active.js b/pages/api/websites/[id]/active.ts similarity index 62% rename from pages/api/websites/[id]/active.js rename to pages/api/websites/[id]/active.ts index 59af938e..b50c29e7 100644 --- a/pages/api/websites/[id]/active.js +++ b/pages/api/websites/[id]/active.ts @@ -3,8 +3,18 @@ import { allowQuery } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { getActiveVisitors } from 'queries'; import { TYPE_WEBSITE } from 'lib/constants'; +import { WebsiteActive } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; -export default async (req, res) => { +export interface WebsiteActiveRequestQuery { + id: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/eventdata.js b/pages/api/websites/[id]/eventdata.ts similarity index 61% rename from pages/api/websites/[id]/eventdata.js rename to pages/api/websites/[id]/eventdata.ts index 0e6ad2e9..646b2920 100644 --- a/pages/api/websites/[id]/eventdata.js +++ b/pages/api/websites/[id]/eventdata.ts @@ -4,8 +4,27 @@ import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics'; import { allowQuery } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { WebsiteMetric } from 'interface/api/models'; -export default async (req, res) => { +export interface WebsiteEventDataRequestQuery { + id: string; +} + +export interface WebsiteEventDataRequestBody { + start_at: string; + end_at: string; + timezone: string; + event_name: string; + columns: { [key: string]: 'count' | 'max' | 'min' | 'avg' | 'sum' }; + filters?: { [key: string]: any }; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/events.js b/pages/api/websites/[id]/events.ts similarity index 53% rename from pages/api/websites/[id]/events.js rename to pages/api/websites/[id]/events.ts index da88794e..efbf5a39 100644 --- a/pages/api/websites/[id]/events.js +++ b/pages/api/websites/[id]/events.ts @@ -1,13 +1,29 @@ -import moment from 'moment-timezone'; -import { getEventMetrics } from 'queries'; -import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics'; +import { WebsiteMetric } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { useAuth, useCors } from 'lib/middleware'; +import moment from 'moment-timezone'; +import { NextApiResponse } from 'next'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventMetrics } from 'queries'; const unitTypes = ['year', 'month', 'hour', 'day']; -export default async (req, res) => { +export interface WebsiteEventsRequestQuery { + id: string; + start_at: string; + end_at: string; + unit: string; + tz: string; + url: string; + event_name: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); @@ -24,9 +40,15 @@ export default async (req, res) => { const startDate = new Date(+start_at); const endDate = new Date(+end_at); - const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, { - url, - eventName: event_name, + const events = await getEventMetrics(websiteId, { + startDate, + endDate, + timezone: tz, + unit, + filters: { + url, + eventName: event_name, + }, }); return ok(res, events); diff --git a/pages/api/websites/[id]/index.js b/pages/api/websites/[id]/index.ts similarity index 72% rename from pages/api/websites/[id]/index.js rename to pages/api/websites/[id]/index.ts index 09056865..834b1732 100644 --- a/pages/api/websites/[id]/index.js +++ b/pages/api/websites/[id]/index.ts @@ -3,8 +3,24 @@ import { useAuth, useCors } from 'lib/middleware'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; import { TYPE_WEBSITE } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; +import { Website } from 'interface/api/models'; -export default async (req, res) => { +export interface WebsiteReqeustQuery { + id: string; +} + +export interface WebsiteReqeustBody { + name: string; + domain: string; + shareId: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/metrics.js b/pages/api/websites/[id]/metrics.ts similarity index 85% rename from pages/api/websites/[id]/metrics.js rename to pages/api/websites/[id]/metrics.ts index 97216273..f3bf38aa 100644 --- a/pages/api/websites/[id]/metrics.js +++ b/pages/api/websites/[id]/metrics.ts @@ -1,6 +1,9 @@ +import { WebsiteMetric } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; import { FILTER_IGNORED, TYPE_WEBSITE } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; +import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getPageviewMetrics, getSessionMetrics, getWebsite } from 'queries'; @@ -33,7 +36,23 @@ function getColumn(type) { return type; } -export default async (req, res) => { +export interface WebsiteMetricsReqeustQuery { + id: string; + type: string; + start_at: number; + end_at: number; + url: string; + referrer: string; + os: string; + browser: string; + device: string; + country: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/pageviews.js b/pages/api/websites/[id]/pageviews.ts similarity index 67% rename from pages/api/websites/[id]/pageviews.js rename to pages/api/websites/[id]/pageviews.ts index 5b628e3a..c85a1a97 100644 --- a/pages/api/websites/[id]/pageviews.js +++ b/pages/api/websites/[id]/pageviews.ts @@ -1,13 +1,34 @@ -import moment from 'moment-timezone'; -import { getPageviewStats } from 'queries'; -import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics'; +import { WebsitePageviews } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { useAuth, useCors } from 'lib/middleware'; +import moment from 'moment-timezone'; +import { NextApiResponse } from 'next'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getPageviewStats } from 'queries'; const unitTypes = ['year', 'month', 'hour', 'day']; -export default async (req, res) => { +export interface WebsitePageviewReqeustQuery { + id: string; + websiteId: string; + start_at: number; + end_at: number; + unit: string; + tz: string; + url?: string; + referrer?: string; + os?: string; + browser?: string; + device?: string; + country?: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); @@ -39,8 +60,8 @@ export default async (req, res) => { const [pageviews, sessions] = await Promise.all([ getPageviewStats(websiteId, { - start_at: startDate, - end_at: endDate, + startDate, + endDate, timezone: tz, unit, count: '*', @@ -54,8 +75,8 @@ export default async (req, res) => { }, }), getPageviewStats(websiteId, { - start_at: startDate, - end_at: endDate, + startDate, + endDate, timezone: tz, unit, count: 'distinct pageview.', diff --git a/pages/api/websites/[id]/reset.js b/pages/api/websites/[id]/reset.ts similarity index 66% rename from pages/api/websites/[id]/reset.js rename to pages/api/websites/[id]/reset.ts index 0dde02df..6d2ffcea 100644 --- a/pages/api/websites/[id]/reset.js +++ b/pages/api/websites/[id]/reset.ts @@ -3,8 +3,17 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { allowQuery } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; -export default async (req, res) => { +export interface WebsiteResetReqeustQuery { + id: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/[id]/stats.js b/pages/api/websites/[id]/stats.ts similarity index 76% rename from pages/api/websites/[id]/stats.js rename to pages/api/websites/[id]/stats.ts index 2c5b0156..0ba65bf2 100644 --- a/pages/api/websites/[id]/stats.js +++ b/pages/api/websites/[id]/stats.ts @@ -3,8 +3,27 @@ import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { allowQuery } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { TYPE_WEBSITE } from 'lib/constants'; +import { WebsiteStats } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; -export default async (req, res) => { +export interface WebsiteStatsReqeustQuery { + id: string; + type: string; + start_at: number; + end_at: number; + url: string; + referrer: string; + os: string; + browser: string; + device: string; + country: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/pages/api/websites/index.js b/pages/api/websites/index.ts similarity index 68% rename from pages/api/websites/index.js rename to pages/api/websites/index.ts index 3966ad70..1b0b0586 100644 --- a/pages/api/websites/index.js +++ b/pages/api/websites/index.ts @@ -2,8 +2,23 @@ import { createWebsite, getAllWebsites, getUserWebsites } from 'queries'; import { ok, methodNotAllowed, getRandomChars } from 'next-basics'; import { useAuth, useCors } from 'lib/middleware'; import { uuid } from 'lib/crypto'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { NextApiResponse } from 'next'; -export default async (req, res) => { +export interface WebsitesReqeustQuery { + include_all?: boolean; +} + +export interface WebsitesReqeustBody { + name: string; + domain: string; + enableShareUrl: boolean; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { await useCors(req, res); await useAuth(req, res); diff --git a/queries/admin/user/createUser.js b/queries/admin/user/createUser.js deleted file mode 100644 index 54e008fe..00000000 --- a/queries/admin/user/createUser.js +++ /dev/null @@ -1,7 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function createUser(data) { - return prisma.client.user.create({ - data, - }); -} diff --git a/queries/admin/user/createUser.ts b/queries/admin/user/createUser.ts new file mode 100644 index 00000000..c1604044 --- /dev/null +++ b/queries/admin/user/createUser.ts @@ -0,0 +1,20 @@ +import prisma from 'lib/prisma'; + +export async function createUser(data: { + id: string; + username: string; + password: string; +}): Promise<{ + id: string; + username: string; + isAdmin: boolean; +}> { + return prisma.client.user.create({ + data, + select: { + id: true, + username: true, + isAdmin: true, + }, + }); +} diff --git a/queries/admin/user/deleteUser.js b/queries/admin/user/deleteUser.ts similarity index 83% rename from queries/admin/user/deleteUser.js rename to queries/admin/user/deleteUser.ts index 5970d2a5..bb556225 100644 --- a/queries/admin/user/deleteUser.js +++ b/queries/admin/user/deleteUser.ts @@ -1,7 +1,10 @@ import prisma from 'lib/prisma'; import cache from 'lib/cache'; +import { Prisma, User } from '@prisma/client'; -export async function deleteUser(userId) { +export async function deleteUser( + userId: string, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Prisma.BatchPayload, User]> { const { client } = prisma; const websites = await client.website.findMany({ diff --git a/queries/admin/user/getUser.js b/queries/admin/user/getUser.js deleted file mode 100644 index 6c9ebd88..00000000 --- a/queries/admin/user/getUser.js +++ /dev/null @@ -1,7 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function getUser(where) { - return prisma.client.user.findUnique({ - where, - }); -} diff --git a/queries/admin/user/getUser.ts b/queries/admin/user/getUser.ts new file mode 100644 index 00000000..c353886e --- /dev/null +++ b/queries/admin/user/getUser.ts @@ -0,0 +1,25 @@ +import prisma from 'lib/prisma'; +import { Prisma } from '@prisma/client'; + +export interface User { + id: string; + username: string; + isAdmin: boolean; + password?: string; + createdAt?: Date; +} + +export async function getUser( + where: Prisma.UserWhereUniqueInput, + includePassword = false, +): Promise { + return prisma.client.user.findUnique({ + where, + select: { + id: true, + username: true, + isAdmin: true, + password: includePassword, + }, + }); +} diff --git a/queries/admin/user/getUsers.js b/queries/admin/user/getUsers.ts similarity index 76% rename from queries/admin/user/getUsers.js rename to queries/admin/user/getUsers.ts index 2fc473cb..e196d232 100644 --- a/queries/admin/user/getUsers.js +++ b/queries/admin/user/getUsers.ts @@ -1,6 +1,7 @@ import prisma from 'lib/prisma'; +import { User } from './getUser'; -export async function getUsers() { +export async function getUsers(): Promise { return prisma.client.user.findMany({ orderBy: [ { isAdmin: 'desc' }, diff --git a/queries/admin/user/updateUser.js b/queries/admin/user/updateUser.js deleted file mode 100644 index ea80cf43..00000000 --- a/queries/admin/user/updateUser.js +++ /dev/null @@ -1,8 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function updateUser(data, where) { - return prisma.client.user.update({ - where, - data, - }); -} diff --git a/queries/admin/user/updateUser.ts b/queries/admin/user/updateUser.ts new file mode 100644 index 00000000..f5a64e51 --- /dev/null +++ b/queries/admin/user/updateUser.ts @@ -0,0 +1,19 @@ +import { Prisma } from '@prisma/client'; +import prisma from 'lib/prisma'; +import { User } from './getUser'; + +export async function updateUser( + data: Prisma.UserUpdateArgs, + where: Prisma.UserWhereUniqueInput, +): Promise { + return prisma.client.user.update({ + where, + data, + select: { + id: true, + username: true, + isAdmin: true, + createdAt: true, + }, + }); +} diff --git a/queries/admin/website/createWebsite.js b/queries/admin/website/createWebsite.ts similarity index 63% rename from queries/admin/website/createWebsite.js rename to queries/admin/website/createWebsite.ts index 0afe0bea..51aa2e3f 100644 --- a/queries/admin/website/createWebsite.js +++ b/queries/admin/website/createWebsite.ts @@ -1,7 +1,16 @@ -import prisma from 'lib/prisma'; +import { Website } from '@prisma/client'; import cache from 'lib/cache'; +import prisma from 'lib/prisma'; -export async function createWebsite(userId, data) { +export async function createWebsite( + userId: string, + data: { + id: string; + name: string; + domain: string; + shareId?: string; + }, +): Promise { return prisma.client.website .create({ data: { diff --git a/queries/admin/website/deleteWebsite.js b/queries/admin/website/deleteWebsite.ts similarity index 52% rename from queries/admin/website/deleteWebsite.js rename to queries/admin/website/deleteWebsite.ts index 685cee8a..a3c57e67 100644 --- a/queries/admin/website/deleteWebsite.js +++ b/queries/admin/website/deleteWebsite.ts @@ -1,22 +1,25 @@ import prisma from 'lib/prisma'; import cache from 'lib/cache'; +import { Prisma, Website } from '@prisma/client'; -export async function deleteWebsite(id) { +export async function deleteWebsite( + websiteId: string, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; return transaction([ client.websiteEvent.deleteMany({ - where: { websiteId: id }, + where: { websiteId }, }), client.session.deleteMany({ - where: { websiteId: id }, + where: { websiteId }, }), client.website.delete({ - where: { id }, + where: { id: websiteId }, }), ]).then(async data => { if (cache.enabled) { - await cache.deleteWebsite(id); + await cache.deleteWebsite(websiteId); } return data; diff --git a/queries/admin/website/getAllWebsites.js b/queries/admin/website/getAllWebsites.js deleted file mode 100644 index f9ad262c..00000000 --- a/queries/admin/website/getAllWebsites.js +++ /dev/null @@ -1,23 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function getAllWebsites() { - let data = await prisma.client.website.findMany({ - orderBy: [ - { - userId: 'asc', - }, - { - name: 'asc', - }, - ], - include: { - user: { - select: { - username: true, - }, - }, - }, - }); - - return data.map(i => ({ ...i, user: i.user.username })); -} diff --git a/queries/admin/website/getAllWebsites.ts b/queries/admin/website/getAllWebsites.ts new file mode 100644 index 00000000..1e68693a --- /dev/null +++ b/queries/admin/website/getAllWebsites.ts @@ -0,0 +1,24 @@ +import { Website } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function getAllWebsites(): Promise<(Website & { user: string })[]> { + return await prisma.client.website + .findMany({ + orderBy: [ + { + userId: 'asc', + }, + { + name: 'asc', + }, + ], + include: { + user: { + select: { + username: true, + }, + }, + }, + }) + .then(data => data.map(i => ({ ...i, user: i.user.username }))); +} diff --git a/queries/admin/website/getUserWebsites.js b/queries/admin/website/getUserWebsites.ts similarity index 59% rename from queries/admin/website/getUserWebsites.js rename to queries/admin/website/getUserWebsites.ts index c1a9d559..f4fa27b9 100644 --- a/queries/admin/website/getUserWebsites.js +++ b/queries/admin/website/getUserWebsites.ts @@ -1,6 +1,7 @@ import prisma from 'lib/prisma'; +import { Website } from '@prisma/client'; -export async function getUserWebsites(userId) { +export async function getUserWebsites(userId): Promise { return prisma.client.website.findMany({ where: { userId, diff --git a/queries/admin/website/getWebsite.js b/queries/admin/website/getWebsite.js deleted file mode 100644 index 83c3e83a..00000000 --- a/queries/admin/website/getWebsite.js +++ /dev/null @@ -1,7 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function getWebsite(where) { - return prisma.client.website.findUnique({ - where, - }); -} diff --git a/queries/admin/website/getWebsite.ts b/queries/admin/website/getWebsite.ts new file mode 100644 index 00000000..9ec27cb9 --- /dev/null +++ b/queries/admin/website/getWebsite.ts @@ -0,0 +1,8 @@ +import prisma from 'lib/prisma'; +import { Prisma, Website } from '@prisma/client'; + +export async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { + return prisma.client.website.findUnique({ + where, + }); +} diff --git a/queries/admin/website/resetWebsite.js b/queries/admin/website/resetWebsite.ts similarity index 51% rename from queries/admin/website/resetWebsite.js rename to queries/admin/website/resetWebsite.ts index f4d685cb..05116f8a 100644 --- a/queries/admin/website/resetWebsite.js +++ b/queries/admin/website/resetWebsite.ts @@ -1,20 +1,23 @@ +import { Prisma, Website } from '@prisma/client'; +import cache from 'lib/cache'; import prisma from 'lib/prisma'; import { getWebsite } from 'queries'; -import cache from 'lib/cache'; -export async function resetWebsite(id) { +export async function resetWebsite( + websiteId, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; - const { revId } = await getWebsite({ id }); + const { revId } = await getWebsite({ id: websiteId }); return transaction([ client.websiteEvent.deleteMany({ - where: { websiteId: id }, + where: { websiteId }, }), client.session.deleteMany({ - where: { websiteId: id }, + where: { websiteId }, }), - client.website.update({ where: { id }, data: { revId: revId + 1 } }), + client.website.update({ where: { id: websiteId }, data: { revId: revId + 1 } }), ]).then(async data => { if (cache.enabled) { await cache.storeWebsite(data[2]); diff --git a/queries/admin/website/updateWebsite.js b/queries/admin/website/updateWebsite.js deleted file mode 100644 index 5ac70a61..00000000 --- a/queries/admin/website/updateWebsite.js +++ /dev/null @@ -1,10 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function updateWebsite(id, data) { - return prisma.client.website.update({ - where: { - id, - }, - data, - }); -} diff --git a/queries/admin/website/updateWebsite.ts b/queries/admin/website/updateWebsite.ts new file mode 100644 index 00000000..51787222 --- /dev/null +++ b/queries/admin/website/updateWebsite.ts @@ -0,0 +1,11 @@ +import { Prisma, Website } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise { + return prisma.client.website.update({ + where: { + id: websiteId, + }, + data, + }); +} diff --git a/queries/analytics/event/getEventData.js b/queries/analytics/event/getEventData.ts similarity index 54% rename from queries/analytics/event/getEventData.js rename to queries/analytics/event/getEventData.ts index f7d725c1..2e776f52 100644 --- a/queries/analytics/event/getEventData.js +++ b/queries/analytics/event/getEventData.ts @@ -2,8 +2,21 @@ import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; +import { WebsiteMetric } from 'interface/api/models'; +import { UmamiApi } from 'interface/enum'; -export async function getEventData(...args) { +export async function getEventData( + ...args: [ + websiteId: string, + data: { + startDate: Date; + endDate: Date; + event_name: string; + columns: any; + filters: object; + }, + ] +): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), @@ -14,31 +27,48 @@ export async function getEventData(...args) { }); } -async function relationalQuery(websiteId, { startDate, endDate, event_name, columns, filters }) { +async function relationalQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + event_name: string; + columns: any; + filters: object; + }, +) { + const { startDate, endDate, event_name, columns, filters } = data; const { rawQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma; const params = [startDate, endDate]; return rawQuery( `select - ${getEventDataColumnsQuery('event_data.event_data', columns)} - from event - join website - on event.website_id = website.website_id - join event_data - on event.event_id = event_data.event_id - where website.website_id ='${websiteId}' - and event.created_at between $1 and $2 + ${getEventDataColumnsQuery('event_data', columns)} + from website_event + where website_id ='${websiteId}' + and created_at between $1 and $2 + and event_type = ${UmamiApi.EventType.Event} ${event_name ? `and event_name = ${event_name}` : ''} ${ Object.keys(filters).length > 0 - ? `and ${getEventDataFilterQuery('event_data.event_data', filters)}` + ? `and ${getEventDataFilterQuery('event_data', filters)}` : '' }`, params, ); } -async function clickhouseQuery(websiteId, { startDate, endDate, event_name, columns, filters }) { +async function clickhouseQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + event_name: string; + columns: any; + filters: object; + }, +) { + const { startDate, endDate, event_name, columns, filters } = data; const { rawQuery, getBetweenDates, getEventDataColumnsQuery, getEventDataFilterQuery } = clickhouse; const website = await cache.fetchWebsite(websiteId); @@ -50,6 +80,7 @@ async function clickhouseQuery(websiteId, { startDate, endDate, event_name, colu from event where website_id = $1 and rev_id = $2 + and event_type = ${UmamiApi.EventType.Event} ${event_name ? `and event_name = ${event_name}` : ''} and ${getBetweenDates('created_at', startDate, endDate)} ${ diff --git a/queries/analytics/event/getEventMetrics.js b/queries/analytics/event/getEventMetrics.js deleted file mode 100644 index 27ee6d04..00000000 --- a/queries/analytics/event/getEventMetrics.js +++ /dev/null @@ -1,68 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import cache from 'lib/cache'; - -export async function getEventMetrics(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery( - websiteId, - start_at, - end_at, - timezone = 'utc', - unit = 'day', - filters = {}, -) { - const { rawQuery, getDateQuery, getFilterQuery } = prisma; - const params = [start_at, end_at]; - - return rawQuery( - `select - event_name x, - ${getDateQuery('event.created_at', unit, timezone)} t, - count(*) y - from event - join website - on event.website_id = website.website_id - where website.website_id='${websiteId}' - and event.created_at between $1 and $2 - ${getFilterQuery('event', filters, params)} - group by 1, 2 - order by 2`, - params, - ); -} - -async function clickhouseQuery( - websiteId, - start_at, - end_at, - timezone = 'UTC', - unit = 'day', - filters = {}, -) { - const { rawQuery, getDateQuery, getBetweenDates, getFilterQuery } = clickhouse; - const website = await cache.fetchWebsite(websiteId); - const params = [websiteId, website?.revId || 0]; - - return rawQuery( - `select - event_name x, - ${getDateQuery('created_at', unit, timezone)} t, - count(*) y - from event - where event_name != '' - and website_id = $1 - and rev_id = $2 - and ${getBetweenDates('created_at', start_at, end_at)} - ${getFilterQuery('event', filters, params)} - group by x, t - order by t`, - params, - ); -} diff --git a/queries/analytics/event/getEventMetrics.ts b/queries/analytics/event/getEventMetrics.ts new file mode 100644 index 00000000..c2e819c4 --- /dev/null +++ b/queries/analytics/event/getEventMetrics.ts @@ -0,0 +1,105 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; +import cache from 'lib/cache'; +import { WebsiteEventMetric } from 'interface/api/models'; +import { UmamiApi } from 'interface/enum'; + +export async function getEventMetrics( + ...args: [ + websiteId: string, + data: { + startDate: Date; + endDate: Date; + timezone: string; + unit: string; + filters: { + url: string; + eventName: string; + }; + }, + ] +): Promise { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + { + startDate, + endDate, + timezone = 'utc', + unit = 'day', + filters, + }: { + startDate: Date; + endDate: Date; + timezone: string; + unit: string; + filters: { + url: string; + eventName: string; + }; + }, +) { + const { rawQuery, getDateQuery, getFilterQuery } = prisma; + const params = [startDate, endDate]; + + return rawQuery( + `select + event_name x, + ${getDateQuery('created_at', unit, timezone)} t, + count(*) y + from website_event + where website_id='${websiteId}' + and created_at between $1 and $2 + and event_type = ${UmamiApi.EventType.Event} + ${getFilterQuery(filters, params)} + group by 1, 2 + order by 2`, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + { + startDate, + endDate, + timezone = 'utc', + unit = 'day', + filters, + }: { + startDate: Date; + endDate: Date; + timezone: string; + unit: string; + filters: { + url: string; + eventName: string; + }; + }, +) { + const { rawQuery, getDateQuery, getBetweenDates, getFilterQuery } = clickhouse; + const website = await cache.fetchWebsite(websiteId); + const params = [websiteId, website?.revId || 0]; + + return rawQuery( + `select + event_name x, + ${getDateQuery('created_at', unit, timezone)} t, + count(*) y + from event + where website_id = $1 + and rev_id = $2 + and event_type = ${UmamiApi.EventType.Event} + and ${getBetweenDates('created_at', startDate, endDate)} + ${getFilterQuery(filters, params)} + group by x, t + order by t`, + params, + ); +} diff --git a/queries/analytics/event/getEvents.js b/queries/analytics/event/getEvents.js deleted file mode 100644 index 81a187ce..00000000 --- a/queries/analytics/event/getEvents.js +++ /dev/null @@ -1,45 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; - -export function getEvents(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -function relationalQuery(websites, start_at) { - return prisma.client.event.findMany({ - where: { - websiteId: { - in: websites, - }, - createdAt: { - gte: start_at, - }, - }, - }); -} - -function clickhouseQuery(websites, start_at) { - const { rawQuery, getDateFormat, getCommaSeparatedStringFormat } = clickhouse; - - return rawQuery( - `select - event_id, - website_id, - session_id, - created_at, - url, - event_name - from event - where event_name != '' - and ${ - websites && websites.length > 0 - ? `website_id in (${getCommaSeparatedStringFormat(websites)})` - : '0 = 0' - } - and created_at >= ${getDateFormat(start_at)}`, - ); -} diff --git a/queries/analytics/event/saveEvent.js b/queries/analytics/event/saveEvent.js deleted file mode 100644 index 3bb3b0bf..00000000 --- a/queries/analytics/event/saveEvent.js +++ /dev/null @@ -1,62 +0,0 @@ -import { EVENT_NAME_LENGTH, URL_LENGTH } from 'lib/constants'; -import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; -import kafka from 'lib/kafka'; -import prisma from 'lib/prisma'; -import { uuid } from 'lib/crypto'; -import cache from 'lib/cache'; - -export async function saveEvent(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(data) { - const { websiteId, sessionId, url, eventName, eventData } = data; - const eventId = uuid(); - - const params = { - id: eventId, - websiteId, - sessionId, - url: url?.substring(0, URL_LENGTH), - eventName: eventName?.substring(0, EVENT_NAME_LENGTH), - }; - - if (eventData) { - params.eventData = { - create: { - id: eventId, - eventData: eventData, - }, - }; - } - - return prisma.client.event.create({ - data: params, - }); -} - -async function clickhouseQuery(data) { - const { websiteId, id: sessionId, url, eventName, eventData, country, ...args } = data; - const { getDateFormat, sendMessage } = kafka; - const website = await cache.fetchWebsite(websiteId); - - const params = { - website_id: websiteId, - session_id: sessionId, - event_id: uuid(), - url: url?.substring(0, URL_LENGTH), - event_name: eventName?.substring(0, EVENT_NAME_LENGTH), - event_data: eventData ? JSON.stringify(eventData) : null, - rev_id: website?.revId || 0, - created_at: getDateFormat(new Date()), - country: country ? country : null, - ...args, - }; - - await sendMessage(params, 'event'); - - return data; -} diff --git a/queries/analytics/event/saveEvent.ts b/queries/analytics/event/saveEvent.ts new file mode 100644 index 00000000..6bb44ef5 --- /dev/null +++ b/queries/analytics/event/saveEvent.ts @@ -0,0 +1,91 @@ +import { EVENT_NAME_LENGTH, URL_LENGTH } from 'lib/constants'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import kafka from 'lib/kafka'; +import prisma from 'lib/prisma'; +import { uuid } from 'lib/crypto'; +import cache from 'lib/cache'; +import { UmamiApi } from 'interface/enum'; + +export async function saveEvent(args: { + id: string; + websiteId: string; + url: string; + referrer?: string; + eventName?: string; + eventData?: any; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; +}) { + return runQuery({ + [PRISMA]: () => relationalQuery(args), + [CLICKHOUSE]: () => clickhouseQuery(args), + }); +} + +async function relationalQuery(data: { + id: string; + websiteId: string; + url: string; + referrer?: string; + eventName?: string; + eventData?: any; +}) { + const { websiteId, id: sessionId, url, eventName, eventData, referrer } = data; + + const params = { + id: uuid(), + websiteId, + sessionId, + url: url?.substring(0, URL_LENGTH), + referrer: referrer?.substring(0, URL_LENGTH), + eventType: UmamiApi.EventType.Event, + eventName: eventName?.substring(0, EVENT_NAME_LENGTH), + eventData, + }; + + return prisma.client.websiteEvent.create({ + data: params, + }); +} + +async function clickhouseQuery(data: { + id: string; + websiteId: string; + url: string; + referrer?: string; + eventName?: string; + eventData?: any; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; +}) { + const { websiteId, id: sessionId, url, eventName, eventData, country, ...args } = data; + const { getDateFormat, sendMessage } = kafka; + const website = await cache.fetchWebsite(websiteId); + + const params = { + website_id: websiteId, + session_id: sessionId, + event_id: uuid(), + url: url?.substring(0, URL_LENGTH), + event_name: eventName?.substring(0, EVENT_NAME_LENGTH), + event_data: eventData ? JSON.stringify(eventData) : null, + rev_id: website?.revId || 0, + created_at: getDateFormat(new Date()), + country: country ? country : null, + ...args, + }; + + await sendMessage(params, 'event'); + + return data; +} diff --git a/queries/analytics/pageview/getPageviewMetrics.js b/queries/analytics/pageview/getPageviewMetrics.js deleted file mode 100644 index 8dfd6595..00000000 --- a/queries/analytics/pageview/getPageviewMetrics.js +++ /dev/null @@ -1,59 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import cache from 'lib/cache'; - -export async function getPageviewMetrics(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(websiteId, { startDate, endDate, column, table, filters = {} }) { - const { rawQuery, parseFilters } = prisma; - const params = [startDate, endDate]; - const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters( - table, - column, - filters, - params, - ); - - return rawQuery( - `select ${column} x, count(*) y - from ${table} - ${` join website on ${table}.website_id = website.website_id`} - ${joinSession} - where website.website_id='${websiteId}' - and ${table}.created_at between $1 and $2 - ${pageviewQuery} - ${joinSession && sessionQuery} - ${eventQuery} - group by 1 - order by 2 desc`, - params, - ); -} - -async function clickhouseQuery(websiteId, { startDate, endDate, column, filters = {} }) { - const { rawQuery, parseFilters, getBetweenDates } = clickhouse; - const website = await cache.fetchWebsite(websiteId); - const params = [websiteId, website?.revId || 0]; - const { pageviewQuery, sessionQuery, eventQuery } = parseFilters(column, filters, params); - - return rawQuery( - `select ${column} x, count(*) y - from event - where website_id = $1 - and rev_id = $2 - ${column !== 'event_name' ? `and event_name = ''` : `and event_name != ''`} - and ${getBetweenDates('created_at', startDate, endDate)} - ${pageviewQuery} - ${sessionQuery} - ${eventQuery} - group by x - order by y desc`, - params, - ); -} diff --git a/queries/analytics/pageview/getPageviewMetrics.ts b/queries/analytics/pageview/getPageviewMetrics.ts new file mode 100644 index 00000000..106d14a9 --- /dev/null +++ b/queries/analytics/pageview/getPageviewMetrics.ts @@ -0,0 +1,82 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; +import cache from 'lib/cache'; +import { Prisma } from '@prisma/client'; +import { UmamiApi } from 'interface/enum'; + +export async function getPageviewMetrics( + ...args: [ + websiteId: string, + data: { + startDate: Date; + endDate: Date; + column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum; + table: string; + filters: object; + }, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum; + filters: object; + }, +) { + const { startDate, endDate, column, filters = {} } = data; + const { rawQuery, parseFilters } = prisma; + const params = [startDate, endDate]; + const { filterQuery, joinSession } = parseFilters(filters, params); + + return rawQuery( + `select ${column} x, count(*) y + from website_event + ${joinSession} + where website_id='${websiteId}' + and website_event.created_at between $1 and $2 + and event_type = ${UmamiApi.EventType.Pageview} + ${filterQuery} + group by 1 + order by 2 desc`, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + column: Prisma.WebsiteEventScalarFieldEnum | Prisma.SessionScalarFieldEnum; + filters: object; + }, +) { + const { startDate, endDate, column, filters = {} } = data; + const { rawQuery, parseFilters, getBetweenDates } = clickhouse; + const website = await cache.fetchWebsite(websiteId); + const params = [websiteId, website?.revId || 0]; + const { filterQuery } = parseFilters(filters, params); + + return rawQuery( + `select ${column} x, count(*) y + from event + where website_id = $1 + and rev_id = $2 + and event_type = ${UmamiApi.EventType.Pageview} + ${column !== 'event_name' ? `and event_name = ''` : `and event_name != ''`} + and ${getBetweenDates('created_at', startDate, endDate)} + ${filterQuery} + group by x + order by y desc`, + params, + ); +} diff --git a/queries/analytics/pageview/getPageviewParams.js b/queries/analytics/pageview/getPageviewParams.js deleted file mode 100644 index ce96c25b..00000000 --- a/queries/analytics/pageview/getPageviewParams.js +++ /dev/null @@ -1,41 +0,0 @@ -import prisma from 'lib/prisma'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; - -export async function getPageviewParams(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(websiteId, start_at, end_at, column, table, filters = {}) { - const { parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; - const { pageviewQuery, sessionQuery, eventQuery, joinSession } = parseFilters( - table, - column, - filters, - params, - ); - - return rawQuery( - `select url x, - count(*) y - from ${table} - ${` join website on ${table}.website_id = website.website_id`} - ${joinSession} - where website.website_id='${websiteId}' - and ${table}.created_at between $1 and $2 - and ${table}.url like '%?%' - ${pageviewQuery} - ${joinSession && sessionQuery} - ${eventQuery} - group by 1 - order by 2 desc`, - params, - ); -} - -function clickhouseQuery() { - return Promise.reject(new Error('Not implemented.')); -} diff --git a/queries/analytics/pageview/getPageviewStats.js b/queries/analytics/pageview/getPageviewStats.js deleted file mode 100644 index c711d448..00000000 --- a/queries/analytics/pageview/getPageviewStats.js +++ /dev/null @@ -1,78 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -import cache from 'lib/cache'; - -export async function getPageviewStats(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery( - websiteId, - { - start_at, - end_at, - timezone = 'utc', - unit = 'day', - count = '*', - filters = {}, - sessionKey = 'session_id', - }, -) { - const { getDateQuery, parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; - const { pageviewQuery, sessionQuery, joinSession } = parseFilters( - 'pageview', - null, - filters, - params, - ); - - return rawQuery( - `select ${getDateQuery('pageview.created_at', unit, timezone)} t, - count(${count !== '*' ? `${count}${sessionKey}` : count}) y - from pageview - join website - on pageview.website_id = website.website_id - ${joinSession} - where website.website_id='${websiteId}' - and pageview.created_at between $1 and $2 - ${pageviewQuery} - ${sessionQuery} - group by 1`, - params, - ); -} - -async function clickhouseQuery( - websiteId, - { start_at, end_at, timezone = 'UTC', unit = 'day', count = '*', filters = {} }, -) { - const { parseFilters, rawQuery, getDateStringQuery, getDateQuery, getBetweenDates } = clickhouse; - const website = await cache.fetchWebsite(websiteId); - const params = [websiteId, website?.revId || 0]; - const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params); - - return rawQuery( - `select - ${getDateStringQuery('g.t', unit)} as t, - g.y as y - from - (select - ${getDateQuery('created_at', unit, timezone)} t, - count(${count !== '*' ? 'distinct session_id' : count}) y - from event - where event_name = '' - and website_id = $1 - and rev_id = $2 - and ${getBetweenDates('created_at', start_at, end_at)} - ${pageviewQuery} - ${sessionQuery} - group by t) g - order by t`, - params, - ); -} diff --git a/queries/analytics/pageview/getPageviewStats.ts b/queries/analytics/pageview/getPageviewStats.ts new file mode 100644 index 00000000..4a7a7782 --- /dev/null +++ b/queries/analytics/pageview/getPageviewStats.ts @@ -0,0 +1,102 @@ +import cache from 'lib/cache'; +import clickhouse from 'lib/clickhouse'; +import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; +import prisma from 'lib/prisma'; +import { UmamiApi } from 'interface/enum'; + +export async function getPageviewStats( + ...args: [ + websiteId: string, + data: { + startDate: Date; + endDate: Date; + timezone?: string; + unit?: string; + count?: string; + filters: object; + sessionKey?: string; + }, + ] +) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + timezone?: string; + unit?: string; + count?: string; + filters: object; + sessionKey?: string; + }, +) { + const { + startDate, + endDate, + timezone = 'utc', + unit = 'day', + count = '*', + filters = {}, + sessionKey = 'session_id', + } = data; + const { getDateQuery, parseFilters, rawQuery } = prisma; + const params = [startDate, endDate]; + const { filterQuery, joinSession } = parseFilters(filters, params); + + return rawQuery( + `select ${getDateQuery('website_event.created_at', unit, timezone)} t, + count(${count !== '*' ? `${count}${sessionKey}` : count}) y + from website_event + ${joinSession} + where website.website_id='${websiteId}' + and pageview.created_at between $1 and $2 + and event_type = ${UmamiApi.EventType.Pageview} + ${filterQuery} + group by 1`, + params, + ); +} + +async function clickhouseQuery( + websiteId: string, + data: { + startDate: Date; + endDate: Date; + timezone?: string; + unit?: string; + count?: string; + filters: object; + sessionKey?: string; + }, +) { + const { startDate, endDate, timezone = 'UTC', unit = 'day', count = '*', filters = {} } = data; + const { parseFilters, rawQuery, getDateStringQuery, getDateQuery, getBetweenDates } = clickhouse; + const website = await cache.fetchWebsite(websiteId); + const params = [websiteId, website?.revId || 0]; + const { filterQuery } = parseFilters(filters, params); + + return rawQuery( + `select + ${getDateStringQuery('g.t', unit)} as t, + g.y as y + from + (select + ${getDateQuery('created_at', unit, timezone)} t, + count(${count !== '*' ? 'distinct session_id' : count}) y + from event + where website_id = $1 + and rev_id = $2 + and event_type = ${UmamiApi.EventType.Pageview} + and ${getBetweenDates('created_at', startDate, endDate)} + ${filterQuery} + group by t) g + order by t`, + params, + ); +} diff --git a/queries/analytics/pageview/getPageviews.js b/queries/analytics/pageview/getPageviews.js deleted file mode 100644 index 2bf41b0b..00000000 --- a/queries/analytics/pageview/getPageviews.js +++ /dev/null @@ -1,43 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; - -export async function getPageviews(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(websites, start_at) { - return prisma.client.pageview.findMany({ - where: { - websiteId: { - in: websites, - }, - createdAt: { - gte: start_at, - }, - }, - }); -} - -async function clickhouseQuery(websites, start_at) { - const { rawQuery, getCommaSeparatedStringFormat } = clickhouse; - - return rawQuery( - `select - website_id, - session_id, - created_at, - url - from event - where event_name = '' - and ${ - websites && websites.length > 0 - ? `website_id in (${getCommaSeparatedStringFormat(websites)})` - : '0 = 0' - } - and created_at >= ${clickhouse.getDateFormat(start_at)}`, - ); -} diff --git a/queries/analytics/pageview/savePageView.js b/queries/analytics/pageview/savePageView.ts similarity index 60% rename from queries/analytics/pageview/savePageView.js rename to queries/analytics/pageview/savePageView.ts index adcb4b3f..134dfd09 100644 --- a/queries/analytics/pageview/savePageView.js +++ b/queries/analytics/pageview/savePageView.ts @@ -4,23 +4,43 @@ import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; import { uuid } from 'lib/crypto'; +import { UmamiApi } from 'interface/enum'; -export async function savePageView(...args) { +export async function savePageView(args: { + id: string; + websiteId: string; + url: string; + referrer?: string; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; +}) { return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), + [PRISMA]: () => relationalQuery(args), + [CLICKHOUSE]: () => clickhouseQuery(args), }); } -async function relationalQuery(data) { - const { websiteId, sessionId, url, referrer } = data; - return prisma.client.pageview.create({ +async function relationalQuery(data: { + id: string; + websiteId: string; + url: string; + referrer?: string; +}) { + const { websiteId, id: sessionId, url, referrer } = data; + + return prisma.client.websiteEvent.create({ data: { id: uuid(), websiteId, sessionId, url: url?.substring(0, URL_LENGTH), referrer: referrer?.substring(0, URL_LENGTH), + eventType: UmamiApi.EventType.Pageview, }, }); } diff --git a/queries/analytics/session/createSession.js b/queries/analytics/session/createSession.ts similarity index 63% rename from queries/analytics/session/createSession.js rename to queries/analytics/session/createSession.ts index f401a20f..fe15f11c 100644 --- a/queries/analytics/session/createSession.js +++ b/queries/analytics/session/createSession.ts @@ -2,11 +2,12 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; +import { Prisma } from '@prisma/client'; -export async function createSession(...args) { +export async function createSession(args: Prisma.SessionCreateInput) { return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), + [PRISMA]: () => relationalQuery(args), + [CLICKHOUSE]: () => clickhouseQuery(args), }).then(async data => { if (cache.enabled) { await cache.storeSession(data); @@ -16,11 +17,21 @@ export async function createSession(...args) { }); } -async function relationalQuery(data) { +async function relationalQuery(data: Prisma.SessionCreateInput) { return prisma.client.session.create({ data }); } -async function clickhouseQuery(data) { +async function clickhouseQuery(data: { + id: string; + websiteId: string; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; +}) { const { id, websiteId, hostname, browser, os, device, screen, language, country } = data; const { getDateFormat, sendMessage } = kafka; const website = await cache.fetchWebsite(websiteId); diff --git a/queries/analytics/session/getSession.js b/queries/analytics/session/getSession.ts similarity index 64% rename from queries/analytics/session/getSession.js rename to queries/analytics/session/getSession.ts index adc9acd8..19875117 100644 --- a/queries/analytics/session/getSession.js +++ b/queries/analytics/session/getSession.ts @@ -1,21 +1,22 @@ import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; +import { Prisma } from '@prisma/client'; -export async function getSession(...args) { +export async function getSession(args: { id: string }) { return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), + [PRISMA]: () => relationalQuery(args), + [CLICKHOUSE]: () => clickhouseQuery(args), }); } -async function relationalQuery(where) { +async function relationalQuery(where: Prisma.SessionWhereUniqueInput) { return prisma.client.session.findUnique({ where, }); } -async function clickhouseQuery({ id: sessionId }) { +async function clickhouseQuery({ id: sessionId }: { id: string }) { const { rawQuery, findFirst } = clickhouse; const params = [sessionId]; diff --git a/queries/analytics/session/getSessionMetrics.js b/queries/analytics/session/getSessionMetrics.ts similarity index 63% rename from queries/analytics/session/getSessionMetrics.js rename to queries/analytics/session/getSessionMetrics.ts index 6e74e9b6..77bc5a63 100644 --- a/queries/analytics/session/getSessionMetrics.js +++ b/queries/analytics/session/getSessionMetrics.ts @@ -3,17 +3,26 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; -export async function getSessionMetrics(...args) { +export async function getSessionMetrics( + ...args: [ + websiteId: string, + data: { startDate: Date; endDate: Date; field: string; filters: object }, + ] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId, { startDate, endDate, field, filters = {} }) { +async function relationalQuery( + websiteId: string, + data: { startDate: Date; endDate: Date; field: string; filters: object }, +) { + const { startDate, endDate, field, filters = {} } = data; const { parseFilters, rawQuery } = prisma; const params = [startDate, endDate]; - const { pageviewQuery, sessionQuery, joinSession } = parseFilters(null, filters, params); + const { filterQuery, joinSession } = parseFilters(filters, params); return rawQuery( `select ${field} x, count(*) y @@ -26,8 +35,7 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters = ${joinSession} where website.website_id='${websiteId}' and pageview.created_at between $1 and $2 - ${pageviewQuery} - ${sessionQuery} + ${filterQuery} ) group by 1 order by 2 desc`, @@ -35,11 +43,15 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters = ); } -async function clickhouseQuery(websiteId, { startDate, endDate, field, filters = {} }) { +async function clickhouseQuery( + websiteId: string, + data: { startDate: Date; endDate: Date; field: string; filters: object }, +) { + const { startDate, endDate, field, filters = {} } = data; const { parseFilters, getBetweenDates, rawQuery } = clickhouse; const website = await cache.fetchWebsite(websiteId); const params = [websiteId, website?.revId || 0]; - const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params); + const { filterQuery } = parseFilters(filters, params); return rawQuery( `select ${field} x, count(*) y @@ -48,8 +60,7 @@ async function clickhouseQuery(websiteId, { startDate, endDate, field, filters = and rev_id = $2 and event_name = '' and ${getBetweenDates('created_at', startDate, endDate)} - ${pageviewQuery} - ${sessionQuery} + ${filterQuery} group by x order by y desc`, params, diff --git a/queries/analytics/session/getSessions.js b/queries/analytics/session/getSessions.js deleted file mode 100644 index c5fed485..00000000 --- a/queries/analytics/session/getSessions.js +++ /dev/null @@ -1,52 +0,0 @@ -import prisma from 'lib/prisma'; -import clickhouse from 'lib/clickhouse'; -import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; - -export async function getSessions(...args) { - return runQuery({ - [PRISMA]: () => relationalQuery(...args), - [CLICKHOUSE]: () => clickhouseQuery(...args), - }); -} - -async function relationalQuery(websites, start_at) { - return prisma.client.session.findMany({ - where: { - ...(websites && websites.length > 0 - ? { - websiteId: { - in: websites, - }, - } - : {}), - createdAt: { - gte: start_at, - }, - }, - }); -} - -async function clickhouseQuery(websites, start_at) { - const { rawQuery, getDateFormat, getCommaSeparatedStringFormat } = clickhouse; - - return rawQuery( - `select distinct - session_id, - website_id, - created_at, - hostname, - browser, - os, - device, - screen, - language, - country - from event - where ${ - websites && websites.length > 0 - ? `website_id in (${getCommaSeparatedStringFormat(websites)})` - : '0 = 0' - } - and created_at >= ${getDateFormat(start_at)}`, - ); -} diff --git a/queries/analytics/stats/getActiveVisitors.js b/queries/analytics/stats/getActiveVisitors.ts similarity index 83% rename from queries/analytics/stats/getActiveVisitors.js rename to queries/analytics/stats/getActiveVisitors.ts index c7592e5b..0b07574d 100644 --- a/queries/analytics/stats/getActiveVisitors.js +++ b/queries/analytics/stats/getActiveVisitors.ts @@ -3,14 +3,14 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -export async function getActiveVisitors(...args) { +export async function getActiveVisitors(...args: [websiteId: string]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId) { +async function relationalQuery(websiteId: string) { const date = subMinutes(new Date(), 5); const params = [date]; @@ -25,7 +25,7 @@ async function relationalQuery(websiteId) { ); } -async function clickhouseQuery(websiteId) { +async function clickhouseQuery(websiteId: string) { const { rawQuery, getDateFormat } = clickhouse; const params = [websiteId]; diff --git a/queries/analytics/stats/getRealtimeData.js b/queries/analytics/stats/getRealtimeData.js deleted file mode 100644 index 659f6145..00000000 --- a/queries/analytics/stats/getRealtimeData.js +++ /dev/null @@ -1,30 +0,0 @@ -import { getPageviews } from '../pageview/getPageviews'; -import { getSessions } from '../session/getSessions'; -import { getEvents } from '../event/getEvents'; - -export async function getRealtimeData(websites, time) { - const [pageviews, sessions, events] = await Promise.all([ - getPageviews(websites, time), - getSessions(websites, time), - getEvents(websites, time), - ]); - - return { - pageviews: pageviews.map(({ id, ...props }) => ({ - __id: `p${id}`, - pageviewId: id, - ...props, - })), - sessions: sessions.map(({ id, ...props }) => ({ - __id: `s${id}`, - sessionId: id, - ...props, - })), - events: events.map(({ id, ...props }) => ({ - __id: `e${id}`, - eventId: id, - ...props, - })), - timestamp: Date.now(), - }; -} diff --git a/queries/analytics/stats/getRealtimeData.ts b/queries/analytics/stats/getRealtimeData.ts new file mode 100644 index 00000000..042bcc46 --- /dev/null +++ b/queries/analytics/stats/getRealtimeData.ts @@ -0,0 +1,3 @@ +export async function getRealtimeData() { + throw new Error('Not Implemented'); +} diff --git a/queries/analytics/stats/getWebsiteStats.js b/queries/analytics/stats/getWebsiteStats.ts similarity index 70% rename from queries/analytics/stats/getWebsiteStats.js rename to queries/analytics/stats/getWebsiteStats.ts index 002d8a9c..bf5cdd96 100644 --- a/queries/analytics/stats/getWebsiteStats.js +++ b/queries/analytics/stats/getWebsiteStats.ts @@ -3,22 +3,23 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; -export async function getWebsiteStats(...args) { +export async function getWebsiteStats( + ...args: [websiteId: string, data: { startDate: Date; endDate: Date; filters: object }] +) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) { +async function relationalQuery( + websiteId: string, + data: { startDate: Date; endDate: Date; filters: object }, +) { + const { startDate, endDate, filters = {} } = data; const { getDateQuery, getTimestampInterval, parseFilters, rawQuery } = prisma; - const params = [start_at, end_at]; - const { pageviewQuery, sessionQuery, joinSession } = parseFilters( - 'pageview', - null, - filters, - params, - ); + const params = [startDate, endDate]; + const { filterQuery, joinSession } = parseFilters(filters, params); return rawQuery( `select sum(t.c) as "pageviews", @@ -36,19 +37,22 @@ async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) { ${joinSession} where website.website_id='${websiteId}' and pageview.created_at between $1 and $2 - ${pageviewQuery} - ${sessionQuery} + ${filterQuery} group by 1, 2 ) t`, params, ); } -async function clickhouseQuery(websiteId, { start_at, end_at, filters = {} }) { +async function clickhouseQuery( + websiteId: string, + data: { startDate: Date; endDate: Date; filters: object }, +) { + const { startDate, endDate, filters = {} } = data; const { rawQuery, getDateQuery, getBetweenDates, parseFilters } = clickhouse; const website = await cache.fetchWebsite(websiteId); const params = [websiteId, website?.revId || 0]; - const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params); + const { filterQuery } = parseFilters(filters, params); return rawQuery( `select @@ -66,9 +70,8 @@ async function clickhouseQuery(websiteId, { start_at, end_at, filters = {} }) { where event_name = '' and website_id = $1 and rev_id = $2 - and ${getBetweenDates('created_at', start_at, end_at)} - ${pageviewQuery} - ${sessionQuery} + and ${getBetweenDates('created_at', startDate, endDate)} + ${filterQuery} group by session_id, time_series ) t;`, params, diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..cbb5413f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "outDir": "./build", + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "lib": ["dom", "dom.iterable", "esnext"], + "skipLibCheck": true, + "esModuleInterop": true, + "noImplicitAny": false, + "preserveConstEnums": true, + "removeComments": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "strict": true, + "baseUrl": ".", + "strictNullChecks": false, + "noEmit": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "queries/admin/website/getAllWebsites.ts"], + "exclude": ["node_modules"] +} From f3726e5abf313b32d3a625591486a628b4588004 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 16 Nov 2022 10:32:02 -0800 Subject: [PATCH 02/38] Re-add realtime data --- db/postgresql/schema.prisma | 7 +-- queries/analytics/event/getEvents.js | 45 +++++++++++++++++++ queries/analytics/pageview/getPageviews.js | 43 ++++++++++++++++++ queries/analytics/session/getSessions.js | 52 ++++++++++++++++++++++ queries/analytics/stats/getRealtimeData.ts | 31 ++++++++++++- 5 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 queries/analytics/event/getEvents.js create mode 100644 queries/analytics/pageview/getPageviews.js create mode 100644 queries/analytics/session/getSessions.js diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index b2cb4662..b6a7610a 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -11,7 +11,6 @@ model User { id String @id @unique @map("user_id") @db.Uuid username String @unique @db.VarChar(255) password String @db.VarChar(60) - isAdmin Boolean @default(false) @map("is_admin") createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) groupRole GroupRole[] @@ -20,7 +19,6 @@ model User { teamWebsite TeamWebsite[] teamUser TeamUser[] userWebsite UserWebsite[] - website Website[] @@map("user") } @@ -44,7 +42,6 @@ model Session { model Website { id String @id @unique @map("website_id") @db.Uuid - userId String @map("user_id") @db.Uuid name String @db.VarChar(100) domain String? @db.VarChar(500) shareId String? @unique @map("share_id") @db.VarChar(64) @@ -52,11 +49,9 @@ model Website { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - user User @relation(fields: [userId], references: [id]) teamWebsite TeamWebsite[] userWebsite UserWebsite[] - @@index([userId]) @@index([createdAt]) @@index([shareId]) @@map("website") @@ -117,7 +112,7 @@ model GroupUser { isDeleted Boolean @default(false) @map("is_deleted") group Group @relation(fields: [groupId], references: [id]) - User User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id]) @@map("group_user") } diff --git a/queries/analytics/event/getEvents.js b/queries/analytics/event/getEvents.js new file mode 100644 index 00000000..81a187ce --- /dev/null +++ b/queries/analytics/event/getEvents.js @@ -0,0 +1,45 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; + +export function getEvents(...args) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +function relationalQuery(websites, start_at) { + return prisma.client.event.findMany({ + where: { + websiteId: { + in: websites, + }, + createdAt: { + gte: start_at, + }, + }, + }); +} + +function clickhouseQuery(websites, start_at) { + const { rawQuery, getDateFormat, getCommaSeparatedStringFormat } = clickhouse; + + return rawQuery( + `select + event_id, + website_id, + session_id, + created_at, + url, + event_name + from event + where event_name != '' + and ${ + websites && websites.length > 0 + ? `website_id in (${getCommaSeparatedStringFormat(websites)})` + : '0 = 0' + } + and created_at >= ${getDateFormat(start_at)}`, + ); +} diff --git a/queries/analytics/pageview/getPageviews.js b/queries/analytics/pageview/getPageviews.js new file mode 100644 index 00000000..2bf41b0b --- /dev/null +++ b/queries/analytics/pageview/getPageviews.js @@ -0,0 +1,43 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; + +export async function getPageviews(...args) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websites, start_at) { + return prisma.client.pageview.findMany({ + where: { + websiteId: { + in: websites, + }, + createdAt: { + gte: start_at, + }, + }, + }); +} + +async function clickhouseQuery(websites, start_at) { + const { rawQuery, getCommaSeparatedStringFormat } = clickhouse; + + return rawQuery( + `select + website_id, + session_id, + created_at, + url + from event + where event_name = '' + and ${ + websites && websites.length > 0 + ? `website_id in (${getCommaSeparatedStringFormat(websites)})` + : '0 = 0' + } + and created_at >= ${clickhouse.getDateFormat(start_at)}`, + ); +} diff --git a/queries/analytics/session/getSessions.js b/queries/analytics/session/getSessions.js new file mode 100644 index 00000000..c5fed485 --- /dev/null +++ b/queries/analytics/session/getSessions.js @@ -0,0 +1,52 @@ +import prisma from 'lib/prisma'; +import clickhouse from 'lib/clickhouse'; +import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; + +export async function getSessions(...args) { + return runQuery({ + [PRISMA]: () => relationalQuery(...args), + [CLICKHOUSE]: () => clickhouseQuery(...args), + }); +} + +async function relationalQuery(websites, start_at) { + return prisma.client.session.findMany({ + where: { + ...(websites && websites.length > 0 + ? { + websiteId: { + in: websites, + }, + } + : {}), + createdAt: { + gte: start_at, + }, + }, + }); +} + +async function clickhouseQuery(websites, start_at) { + const { rawQuery, getDateFormat, getCommaSeparatedStringFormat } = clickhouse; + + return rawQuery( + `select distinct + session_id, + website_id, + created_at, + hostname, + browser, + os, + device, + screen, + language, + country + from event + where ${ + websites && websites.length > 0 + ? `website_id in (${getCommaSeparatedStringFormat(websites)})` + : '0 = 0' + } + and created_at >= ${getDateFormat(start_at)}`, + ); +} diff --git a/queries/analytics/stats/getRealtimeData.ts b/queries/analytics/stats/getRealtimeData.ts index 042bcc46..659f6145 100644 --- a/queries/analytics/stats/getRealtimeData.ts +++ b/queries/analytics/stats/getRealtimeData.ts @@ -1,3 +1,30 @@ -export async function getRealtimeData() { - throw new Error('Not Implemented'); +import { getPageviews } from '../pageview/getPageviews'; +import { getSessions } from '../session/getSessions'; +import { getEvents } from '../event/getEvents'; + +export async function getRealtimeData(websites, time) { + const [pageviews, sessions, events] = await Promise.all([ + getPageviews(websites, time), + getSessions(websites, time), + getEvents(websites, time), + ]); + + return { + pageviews: pageviews.map(({ id, ...props }) => ({ + __id: `p${id}`, + pageviewId: id, + ...props, + })), + sessions: sessions.map(({ id, ...props }) => ({ + __id: `s${id}`, + sessionId: id, + ...props, + })), + events: events.map(({ id, ...props }) => ({ + __id: `e${id}`, + eventId: id, + ...props, + })), + timestamp: Date.now(), + }; } From e3142e93f0654fd53e9eb6264881f335000f0d5d Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 17 Nov 2022 18:09:03 -0800 Subject: [PATCH 03/38] get distinct sessions for session metrics --- queries/analytics/session/getSessionMetrics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queries/analytics/session/getSessionMetrics.js b/queries/analytics/session/getSessionMetrics.js index 6e74e9b6..7db0f9b5 100644 --- a/queries/analytics/session/getSessionMetrics.js +++ b/queries/analytics/session/getSessionMetrics.js @@ -42,7 +42,7 @@ async function clickhouseQuery(websiteId, { startDate, endDate, field, filters = const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params); return rawQuery( - `select ${field} x, count(*) y + `select ${field} x, count(distinct session_id) y from event as x where website_id = $1 and rev_id = $2 From 5aa8187e422e345a1a5006f0775351e2a2b59310 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 17 Nov 2022 22:27:33 -0800 Subject: [PATCH 04/38] Add queries for new schema. --- db/postgresql/schema.prisma | 39 +++-- pages/api/websites/[id]/stats.ts | 8 +- pages/api/websites/index.ts | 12 +- queries/admin/permission.ts | 41 ++++++ queries/admin/role.ts | 57 ++++++++ queries/admin/team.ts | 56 ++++++++ queries/admin/teamUser.ts | 41 ++++++ queries/admin/teamWebsite.ts | 43 ++++++ queries/admin/user.ts | 152 ++++++++++++++++++++ queries/admin/user/createUser.ts | 20 --- queries/admin/user/deleteUser.ts | 48 ------- queries/admin/user/getUser.ts | 25 ---- queries/admin/user/getUsers.ts | 19 --- queries/admin/user/updateUser.ts | 19 --- queries/admin/userRole.ts | 41 ++++++ queries/admin/userWebsite.ts | 43 ++++++ queries/admin/website.ts | 176 +++++++++++++++++++++++ queries/admin/website/createWebsite.ts | 32 ----- queries/admin/website/deleteWebsite.ts | 27 ---- queries/admin/website/getAllWebsites.ts | 24 ---- queries/admin/website/getUserWebsites.ts | 13 -- queries/admin/website/getWebsite.ts | 8 -- queries/admin/website/resetWebsite.ts | 28 ---- queries/admin/website/updateWebsite.ts | 11 -- queries/index.js | 22 ++- 25 files changed, 699 insertions(+), 306 deletions(-) create mode 100644 queries/admin/permission.ts create mode 100644 queries/admin/role.ts create mode 100644 queries/admin/team.ts create mode 100644 queries/admin/teamUser.ts create mode 100644 queries/admin/teamWebsite.ts create mode 100644 queries/admin/user.ts delete mode 100644 queries/admin/user/createUser.ts delete mode 100644 queries/admin/user/deleteUser.ts delete mode 100644 queries/admin/user/getUser.ts delete mode 100644 queries/admin/user/getUsers.ts delete mode 100644 queries/admin/user/updateUser.ts create mode 100644 queries/admin/userRole.ts create mode 100644 queries/admin/userWebsite.ts create mode 100644 queries/admin/website.ts delete mode 100644 queries/admin/website/createWebsite.ts delete mode 100644 queries/admin/website/deleteWebsite.ts delete mode 100644 queries/admin/website/getAllWebsites.ts delete mode 100644 queries/admin/website/getUserWebsites.ts delete mode 100644 queries/admin/website/getWebsite.ts delete mode 100644 queries/admin/website/resetWebsite.ts delete mode 100644 queries/admin/website/updateWebsite.ts diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index b6a7610a..538a1fa8 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -12,6 +12,7 @@ model User { username String @unique @db.VarChar(255) password String @db.VarChar(60) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") groupRole GroupRole[] groupUser GroupUser[] @@ -118,11 +119,12 @@ model GroupUser { } model Permission { - id String @id() @unique() @map("permission_id") @db.Uuid - name String @unique() @db.VarChar(255) - description String? @db.VarChar(255) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") + id String @id() @unique() @map("permission_id") @db.Uuid + name String @unique() @db.VarChar(255) + description String? @db.VarChar(255) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") + RolePermission RolePermission[] @@map("permission") } @@ -134,21 +136,37 @@ model Role { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - groupRoles GroupRole[] - userRoles UserRole[] + groupRoles GroupRole[] + userRoles UserRole[] + RolePermission RolePermission[] @@map("role") } +model RolePermission { + id String @id() @unique() @map("role_permission_id") @db.Uuid + roleId String @map("role_id") @db.Uuid + permissionId String @map("permission_id") @db.Uuid + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") + + role Role @relation(fields: [roleId], references: [id]) + permission Permission @relation(fields: [permissionId], references: [id]) + + @@map("role_permission") +} + model UserRole { id String @id() @unique() @map("user_role_id") @db.Uuid roleId String @map("role_id") @db.Uuid userId String @map("user_id") @db.Uuid + teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - role Role @relation(fields: [roleId], references: [id]) - user User @relation(fields: [userId], references: [id]) + role Role @relation(fields: [roleId], references: [id]) + user User @relation(fields: [userId], references: [id]) + team Team? @relation(fields: [teamId], references: [id]) @@map("user_role") } @@ -161,6 +179,7 @@ model Team { teamWebsites TeamWebsite[] teamUsers TeamUser[] + UserRole UserRole[] @@map("team") } @@ -184,6 +203,7 @@ model TeamUser { id String @id() @unique() @map("team_user_id") @db.Uuid teamId String @map("team_id") @db.Uuid userId String @map("user_id") @db.Uuid + isOwner Boolean @default(false) @map("is_owner") createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") @@ -198,6 +218,7 @@ model UserWebsite { userId String @map("user_id") @db.Uuid websiteId String @map("website_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + isDeleted Boolean @default(false) @map("is_deleted") website Website @relation(fields: [websiteId], references: [id]) user User @relation(fields: [userId], references: [id]) diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index 0ba65bf2..d9a581a9 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -52,8 +52,8 @@ export default async ( const prevEndDate = new Date(+end_at - distance); const metrics = await getWebsiteStats(websiteId, { - start_at: startDate, - end_at: endDate, + startDate, + endDate, filters: { url, referrer, @@ -64,8 +64,8 @@ export default async ( }, }); const prevPeriod = await getWebsiteStats(websiteId, { - start_at: prevStartDate, - end_at: prevEndDate, + startDate: prevStartDate, + endDate: prevEndDate, filters: { url, referrer, diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index 1b0b0586..c4f2b3ea 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,9 +1,9 @@ -import { createWebsite, getAllWebsites, getUserWebsites } from 'queries'; -import { ok, methodNotAllowed, getRandomChars } from 'next-basics'; -import { useAuth, useCors } from 'lib/middleware'; -import { uuid } from 'lib/crypto'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { uuid } from 'lib/crypto'; +import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; +import { getRandomChars, methodNotAllowed, ok } from 'next-basics'; +import { createWebsiteByUser, getAllWebsites, getWebsitesByUserId } from 'queries'; export interface WebsitesReqeustQuery { include_all?: boolean; @@ -30,7 +30,7 @@ export default async ( const { include_all } = req.query; const websites = - isAdmin && include_all ? await getAllWebsites() : await getUserWebsites(userId); + isAdmin && include_all ? await getAllWebsites() : await getWebsitesByUserId(userId); return ok(res, websites); } @@ -39,7 +39,7 @@ export default async ( const { name, domain, enableShareUrl } = req.body; const shareId = enableShareUrl ? getRandomChars(8) : null; - const website = await createWebsite(userId, { id: uuid(), name, domain, shareId }); + const website = await createWebsiteByUser(userId, { id: uuid(), name, domain, shareId }); return ok(res, website); } diff --git a/queries/admin/permission.ts b/queries/admin/permission.ts new file mode 100644 index 00000000..37d1647e --- /dev/null +++ b/queries/admin/permission.ts @@ -0,0 +1,41 @@ +import { Prisma, Permission } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createPermission(data: Prisma.PermissionCreateInput): Promise { + return prisma.client.permission.create({ + data, + }); +} + +export async function getPermission(where: Prisma.PermissionWhereUniqueInput): Promise { + return prisma.client.permission.findUnique({ + where, + }); +} + +export async function getPermissions(where: Prisma.PermissionWhereInput): Promise { + return prisma.client.permission.findMany({ + where, + }); +} + +export async function updatePermission( + data: Prisma.PermissionUpdateInput, + where: Prisma.PermissionWhereUniqueInput, +): Promise { + return prisma.client.permission.update({ + data, + where, + }); +} + +export async function deletePermission(permissionId: string): Promise { + return prisma.client.permission.update({ + data: { + isDeleted: true, + }, + where: { + id: permissionId, + }, + }); +} diff --git a/queries/admin/role.ts b/queries/admin/role.ts new file mode 100644 index 00000000..2bf39930 --- /dev/null +++ b/queries/admin/role.ts @@ -0,0 +1,57 @@ +import { Prisma, Role } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createRole(data: { + id: string; + name: string; + description: string; +}): Promise { + return prisma.client.role.create({ + data, + }); +} + +export async function getRole(where: Prisma.RoleWhereUniqueInput): Promise { + return prisma.client.role.findUnique({ + where, + }); +} + +export async function getRoles(where: Prisma.RoleWhereInput): Promise { + return prisma.client.role.findMany({ + where, + }); +} + +export async function getRolesByUserId(userId: string): Promise { + return prisma.client.role.findMany({ + where: { + userRoles: { + every: { + userId, + }, + }, + }, + }); +} + +export async function updateRole( + data: Prisma.RoleUpdateInput, + where: Prisma.RoleWhereUniqueInput, +): Promise { + return prisma.client.role.update({ + data, + where, + }); +} + +export async function deleteRole(roleId: string): Promise { + return prisma.client.role.update({ + data: { + isDeleted: true, + }, + where: { + id: roleId, + }, + }); +} diff --git a/queries/admin/team.ts b/queries/admin/team.ts new file mode 100644 index 00000000..71b5f807 --- /dev/null +++ b/queries/admin/team.ts @@ -0,0 +1,56 @@ +import { Prisma, Role, Team, TeamUser } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeam(data: Prisma.RoleCreateInput): Promise { + return prisma.client.role.create({ + data, + }); +} + +export async function getTeam(where: Prisma.RoleWhereUniqueInput): Promise { + return prisma.client.role.findUnique({ + where, + }); +} + +export async function getTeams(where: Prisma.RoleWhereInput): Promise { + return prisma.client.role.findMany({ + where, + }); +} + +export async function getTeamsByUserId(userId: string): Promise< + (TeamUser & { + team: Team; + })[] +> { + return prisma.client.teamUser.findMany({ + where: { + userId, + }, + include: { + team: true, + }, + }); +} + +export async function updateTeam( + data: Prisma.RoleUpdateInput, + where: Prisma.RoleWhereUniqueInput, +): Promise { + return prisma.client.role.update({ + data, + where, + }); +} + +export async function deleteTeam(teamId: string): Promise { + return prisma.client.role.update({ + data: { + isDeleted: true, + }, + where: { + id: teamId, + }, + }); +} diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts new file mode 100644 index 00000000..e110efcf --- /dev/null +++ b/queries/admin/teamUser.ts @@ -0,0 +1,41 @@ +import { Prisma, TeamUser } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeamUser(data: Prisma.TeamUserCreateInput): Promise { + return prisma.client.teamUser.create({ + data, + }); +} + +export async function getTeamUser(where: Prisma.TeamUserWhereUniqueInput): Promise { + return prisma.client.teamUser.findUnique({ + where, + }); +} + +export async function getTeamUsers(where: Prisma.TeamUserWhereInput): Promise { + return prisma.client.teamUser.findMany({ + where, + }); +} + +export async function updateTeamUser( + data: Prisma.TeamUserUpdateInput, + where: Prisma.TeamUserWhereUniqueInput, +): Promise { + return prisma.client.teamUser.update({ + data, + where, + }); +} + +export async function deleteTeamUser(teamUserId: string): Promise { + return prisma.client.teamUser.update({ + data: { + isDeleted: true, + }, + where: { + id: teamUserId, + }, + }); +} diff --git a/queries/admin/teamWebsite.ts b/queries/admin/teamWebsite.ts new file mode 100644 index 00000000..950a7026 --- /dev/null +++ b/queries/admin/teamWebsite.ts @@ -0,0 +1,43 @@ +import { Prisma, TeamWebsite } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createTeamWebsite(data: Prisma.TeamWebsiteCreateInput): Promise { + return prisma.client.teamWebsite.create({ + data, + }); +} + +export async function getTeamWebsite( + where: Prisma.TeamWebsiteWhereUniqueInput, +): Promise { + return prisma.client.teamWebsite.findUnique({ + where, + }); +} + +export async function getTeamWebsites(where: Prisma.TeamWebsiteWhereInput): Promise { + return prisma.client.teamWebsite.findMany({ + where, + }); +} + +export async function updateTeamWebsite( + data: Prisma.TeamWebsiteUpdateInput, + where: Prisma.TeamWebsiteWhereUniqueInput, +): Promise { + return prisma.client.teamWebsite.update({ + data, + where, + }); +} + +export async function deleteTeamWebsite(teamWebsiteId: string): Promise { + return prisma.client.teamWebsite.update({ + data: { + isDeleted: true, + }, + where: { + id: teamWebsiteId, + }, + }); +} diff --git a/queries/admin/user.ts b/queries/admin/user.ts new file mode 100644 index 00000000..98eadc1b --- /dev/null +++ b/queries/admin/user.ts @@ -0,0 +1,152 @@ +import { Prisma } from '@prisma/client'; +import { UmamiApi } from 'interface/enum'; +import cache from 'lib/cache'; +import prisma from 'lib/prisma'; + +export interface User { + id: string; + username: string; + password?: string; + createdAt?: Date; +} + +export async function createUser(data: { + id: string; + username: string; + password: string; +}): Promise<{ + id: string; + username: string; +}> { + return prisma.client.user.create({ + data, + select: { + id: true, + username: true, + }, + }); +} + +export async function getUser( + where: Prisma.UserWhereUniqueInput, + includePassword = false, +): Promise { + return prisma.client.user.findUnique({ + where, + select: { + id: true, + username: true, + userRole: { + include: { + role: true, + }, + }, + password: includePassword, + }, + }); +} + +export async function getUsers(): Promise { + return prisma.client.user.findMany({ + orderBy: [ + { + username: 'asc', + }, + ], + select: { + id: true, + username: true, + createdAt: true, + }, + }); +} + +export async function getUsersByTeamId(teamId): Promise { + return prisma.client.user.findMany({ + where: { + teamUser: { + every: { + teamId, + }, + }, + }, + select: { + id: true, + username: true, + createdAt: true, + }, + }); +} + +export async function updateUser( + data: Prisma.UserUpdateInput, + where: Prisma.UserWhereUniqueInput, +): Promise { + return prisma.client.user + .update({ + where, + data, + select: { + id: true, + username: true, + createdAt: true, + userRole: true, + }, + }) + .then(user => { + const { userRole, ...rest } = user; + + return { ...rest, isAdmin: userRole.some(a => a.roleId === UmamiApi.SystemRole.Admin) }; + }); +} + +export async function deleteUser( + userId: string, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Prisma.BatchPayload, User]> { + const { client } = prisma; + + const websites = await client.userWebsite.findMany({ + where: { userId }, + }); + + let websiteIds = []; + + if (websites.length > 0) { + websiteIds = websites.map(a => a.websiteId); + } + + return client + .$transaction([ + client.websiteEvent.deleteMany({ + where: { websiteId: { in: websiteIds } }, + }), + client.session.deleteMany({ + where: { websiteId: { in: websiteIds } }, + }), + client.website.updateMany({ + data: { + isDeleted: true, + }, + where: { id: { in: websiteIds } }, + }), + client.user.update({ + data: { + isDeleted: true, + }, + where: { + id: userId, + }, + }), + ]) + .then(async data => { + if (cache.enabled) { + const ids = websites.map(a => a.id); + + for (let i = 0; i < ids.length; i++) { + await cache.deleteWebsite(`website:${ids[i]}`); + } + } + + return data; + }); +} diff --git a/queries/admin/user/createUser.ts b/queries/admin/user/createUser.ts deleted file mode 100644 index c1604044..00000000 --- a/queries/admin/user/createUser.ts +++ /dev/null @@ -1,20 +0,0 @@ -import prisma from 'lib/prisma'; - -export async function createUser(data: { - id: string; - username: string; - password: string; -}): Promise<{ - id: string; - username: string; - isAdmin: boolean; -}> { - return prisma.client.user.create({ - data, - select: { - id: true, - username: true, - isAdmin: true, - }, - }); -} diff --git a/queries/admin/user/deleteUser.ts b/queries/admin/user/deleteUser.ts deleted file mode 100644 index bb556225..00000000 --- a/queries/admin/user/deleteUser.ts +++ /dev/null @@ -1,48 +0,0 @@ -import prisma from 'lib/prisma'; -import cache from 'lib/cache'; -import { Prisma, User } from '@prisma/client'; - -export async function deleteUser( - userId: string, -): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Prisma.BatchPayload, User]> { - const { client } = prisma; - - const websites = await client.website.findMany({ - where: { userId }, - }); - - let websiteIds = []; - - if (websites.length > 0) { - websiteIds = websites.map(a => a.id); - } - - return client - .$transaction([ - client.websiteEvent.deleteMany({ - where: { websiteId: { in: websiteIds } }, - }), - client.session.deleteMany({ - where: { websiteId: { in: websiteIds } }, - }), - client.website.deleteMany({ - where: { userId }, - }), - client.user.delete({ - where: { - id: userId, - }, - }), - ]) - .then(async data => { - if (cache.enabled) { - const ids = websites.map(a => a.id); - - for (let i = 0; i < ids.length; i++) { - await cache.deleteWebsite(`website:${ids[i]}`); - } - } - - return data; - }); -} diff --git a/queries/admin/user/getUser.ts b/queries/admin/user/getUser.ts deleted file mode 100644 index c353886e..00000000 --- a/queries/admin/user/getUser.ts +++ /dev/null @@ -1,25 +0,0 @@ -import prisma from 'lib/prisma'; -import { Prisma } from '@prisma/client'; - -export interface User { - id: string; - username: string; - isAdmin: boolean; - password?: string; - createdAt?: Date; -} - -export async function getUser( - where: Prisma.UserWhereUniqueInput, - includePassword = false, -): Promise { - return prisma.client.user.findUnique({ - where, - select: { - id: true, - username: true, - isAdmin: true, - password: includePassword, - }, - }); -} diff --git a/queries/admin/user/getUsers.ts b/queries/admin/user/getUsers.ts deleted file mode 100644 index e196d232..00000000 --- a/queries/admin/user/getUsers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import prisma from 'lib/prisma'; -import { User } from './getUser'; - -export async function getUsers(): Promise { - return prisma.client.user.findMany({ - orderBy: [ - { isAdmin: 'desc' }, - { - username: 'asc', - }, - ], - select: { - id: true, - username: true, - isAdmin: true, - createdAt: true, - }, - }); -} diff --git a/queries/admin/user/updateUser.ts b/queries/admin/user/updateUser.ts deleted file mode 100644 index f5a64e51..00000000 --- a/queries/admin/user/updateUser.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Prisma } from '@prisma/client'; -import prisma from 'lib/prisma'; -import { User } from './getUser'; - -export async function updateUser( - data: Prisma.UserUpdateArgs, - where: Prisma.UserWhereUniqueInput, -): Promise { - return prisma.client.user.update({ - where, - data, - select: { - id: true, - username: true, - isAdmin: true, - createdAt: true, - }, - }); -} diff --git a/queries/admin/userRole.ts b/queries/admin/userRole.ts new file mode 100644 index 00000000..c4e365ac --- /dev/null +++ b/queries/admin/userRole.ts @@ -0,0 +1,41 @@ +import { Prisma, UserRole } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createUserRole(data: Prisma.UserRoleCreateInput): Promise { + return prisma.client.userRole.create({ + data, + }); +} + +export async function getUserRole(where: Prisma.UserRoleWhereUniqueInput): Promise { + return prisma.client.userRole.findUnique({ + where, + }); +} + +export async function getUserRoles(where: Prisma.UserRoleWhereInput): Promise { + return prisma.client.userRole.findMany({ + where, + }); +} + +export async function updateUserRole( + data: Prisma.UserRoleUpdateInput, + where: Prisma.UserRoleWhereUniqueInput, +): Promise { + return prisma.client.userRole.update({ + data, + where, + }); +} + +export async function deleteUserRole(userRoleId: string): Promise { + return prisma.client.userRole.update({ + data: { + isDeleted: true, + }, + where: { + id: userRoleId, + }, + }); +} diff --git a/queries/admin/userWebsite.ts b/queries/admin/userWebsite.ts new file mode 100644 index 00000000..313d6cd4 --- /dev/null +++ b/queries/admin/userWebsite.ts @@ -0,0 +1,43 @@ +import { Prisma, UserWebsite } from '@prisma/client'; +import prisma from 'lib/prisma'; + +export async function createUserWebsite(data: Prisma.UserWebsiteCreateInput): Promise { + return prisma.client.userWebsite.create({ + data, + }); +} + +export async function getUserWebsite( + where: Prisma.UserWebsiteWhereUniqueInput, +): Promise { + return prisma.client.userWebsite.findUnique({ + where, + }); +} + +export async function getUserWebsites(where: Prisma.UserWebsiteWhereInput): Promise { + return prisma.client.userWebsite.findMany({ + where, + }); +} + +export async function updateUserWebsite( + data: Prisma.UserWebsiteUpdateInput, + where: Prisma.UserWebsiteWhereUniqueInput, +): Promise { + return prisma.client.userWebsite.update({ + data, + where, + }); +} + +export async function deleteUserWebsite(userWebsiteId: string): Promise { + return prisma.client.userWebsite.update({ + data: { + isDeleted: true, + }, + where: { + id: userWebsiteId, + }, + }); +} diff --git a/queries/admin/website.ts b/queries/admin/website.ts new file mode 100644 index 00000000..240798da --- /dev/null +++ b/queries/admin/website.ts @@ -0,0 +1,176 @@ +import { Prisma, Website } from '@prisma/client'; +import cache from 'lib/cache'; +import prisma from 'lib/prisma'; + +export async function createWebsiteByUser( + userId: string, + data: { + id: string; + name: string; + domain: string; + shareId?: string; + }, +): Promise { + return prisma.client.website + .create({ + data: { + userWebsite: { + connect: { + id: userId, + }, + }, + ...data, + }, + }) + .then(async data => { + if (cache.enabled) { + await cache.storeWebsite(data); + } + + return data; + }); +} + +export async function createWebsiteByTeam( + teamId: string, + data: { + id: string; + name: string; + domain: string; + shareId?: string; + }, +): Promise { + return prisma.client.website + .create({ + data: { + teamWebsite: { + connect: { + id: teamId, + }, + }, + ...data, + }, + }) + .then(async data => { + if (cache.enabled) { + await cache.storeWebsite(data); + } + + return data; + }); +} + +export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise { + return prisma.client.website.update({ + where: { + id: websiteId, + }, + data, + }); +} + +export async function resetWebsite( + websiteId, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { + const { client, transaction } = prisma; + + const { revId } = await getWebsite({ id: websiteId }); + + return transaction([ + client.websiteEvent.deleteMany({ + where: { websiteId }, + }), + client.session.deleteMany({ + where: { websiteId }, + }), + client.website.update({ where: { id: websiteId }, data: { revId: revId + 1 } }), + ]).then(async data => { + if (cache.enabled) { + await cache.storeWebsite(data[2]); + } + + return data; + }); +} + +export async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { + return prisma.client.website.findUnique({ + where, + }); +} + +export async function getWebsitesByUserId(userId): Promise { + return prisma.client.website.findMany({ + where: { + userWebsite: { + every: { + userId, + }, + }, + }, + orderBy: { + name: 'asc', + }, + }); +} + +export async function getWebsitesByTeamId(teamId): Promise { + return prisma.client.website.findMany({ + where: { + teamWebsite: { + every: { + teamId, + }, + }, + }, + orderBy: { + name: 'asc', + }, + }); +} + +export async function getAllWebsites(): Promise<(Website & { user: string })[]> { + return await prisma.client.website + .findMany({ + orderBy: [ + { + name: 'asc', + }, + ], + include: { + userWebsite: { + include: { + user: true, + }, + }, + }, + }) + .then(data => data.map(i => ({ ...i, user: i.userWebsite[0]?.userId }))); +} + +export async function deleteWebsite( + websiteId: string, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { + const { client, transaction } = prisma; + + return transaction([ + client.websiteEvent.deleteMany({ + where: { websiteId }, + }), + client.session.deleteMany({ + where: { websiteId }, + }), + client.website.update({ + data: { + isDeleted: true, + }, + where: { id: websiteId }, + }), + ]).then(async data => { + if (cache.enabled) { + await cache.deleteWebsite(websiteId); + } + + return data; + }); +} diff --git a/queries/admin/website/createWebsite.ts b/queries/admin/website/createWebsite.ts deleted file mode 100644 index 51aa2e3f..00000000 --- a/queries/admin/website/createWebsite.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Website } from '@prisma/client'; -import cache from 'lib/cache'; -import prisma from 'lib/prisma'; - -export async function createWebsite( - userId: string, - data: { - id: string; - name: string; - domain: string; - shareId?: string; - }, -): Promise { - return prisma.client.website - .create({ - data: { - user: { - connect: { - id: userId, - }, - }, - ...data, - }, - }) - .then(async data => { - if (cache.enabled) { - await cache.storeWebsite(data); - } - - return data; - }); -} diff --git a/queries/admin/website/deleteWebsite.ts b/queries/admin/website/deleteWebsite.ts deleted file mode 100644 index a3c57e67..00000000 --- a/queries/admin/website/deleteWebsite.ts +++ /dev/null @@ -1,27 +0,0 @@ -import prisma from 'lib/prisma'; -import cache from 'lib/cache'; -import { Prisma, Website } from '@prisma/client'; - -export async function deleteWebsite( - websiteId: string, -): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { - const { client, transaction } = prisma; - - return transaction([ - client.websiteEvent.deleteMany({ - where: { websiteId }, - }), - client.session.deleteMany({ - where: { websiteId }, - }), - client.website.delete({ - where: { id: websiteId }, - }), - ]).then(async data => { - if (cache.enabled) { - await cache.deleteWebsite(websiteId); - } - - return data; - }); -} diff --git a/queries/admin/website/getAllWebsites.ts b/queries/admin/website/getAllWebsites.ts deleted file mode 100644 index 1e68693a..00000000 --- a/queries/admin/website/getAllWebsites.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Website } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function getAllWebsites(): Promise<(Website & { user: string })[]> { - return await prisma.client.website - .findMany({ - orderBy: [ - { - userId: 'asc', - }, - { - name: 'asc', - }, - ], - include: { - user: { - select: { - username: true, - }, - }, - }, - }) - .then(data => data.map(i => ({ ...i, user: i.user.username }))); -} diff --git a/queries/admin/website/getUserWebsites.ts b/queries/admin/website/getUserWebsites.ts deleted file mode 100644 index f4fa27b9..00000000 --- a/queries/admin/website/getUserWebsites.ts +++ /dev/null @@ -1,13 +0,0 @@ -import prisma from 'lib/prisma'; -import { Website } from '@prisma/client'; - -export async function getUserWebsites(userId): Promise { - return prisma.client.website.findMany({ - where: { - userId, - }, - orderBy: { - name: 'asc', - }, - }); -} diff --git a/queries/admin/website/getWebsite.ts b/queries/admin/website/getWebsite.ts deleted file mode 100644 index 9ec27cb9..00000000 --- a/queries/admin/website/getWebsite.ts +++ /dev/null @@ -1,8 +0,0 @@ -import prisma from 'lib/prisma'; -import { Prisma, Website } from '@prisma/client'; - -export async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { - return prisma.client.website.findUnique({ - where, - }); -} diff --git a/queries/admin/website/resetWebsite.ts b/queries/admin/website/resetWebsite.ts deleted file mode 100644 index 05116f8a..00000000 --- a/queries/admin/website/resetWebsite.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Prisma, Website } from '@prisma/client'; -import cache from 'lib/cache'; -import prisma from 'lib/prisma'; -import { getWebsite } from 'queries'; - -export async function resetWebsite( - websiteId, -): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { - const { client, transaction } = prisma; - - const { revId } = await getWebsite({ id: websiteId }); - - return transaction([ - client.websiteEvent.deleteMany({ - where: { websiteId }, - }), - client.session.deleteMany({ - where: { websiteId }, - }), - client.website.update({ where: { id: websiteId }, data: { revId: revId + 1 } }), - ]).then(async data => { - if (cache.enabled) { - await cache.storeWebsite(data[2]); - } - - return data; - }); -} diff --git a/queries/admin/website/updateWebsite.ts b/queries/admin/website/updateWebsite.ts deleted file mode 100644 index 51787222..00000000 --- a/queries/admin/website/updateWebsite.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Prisma, Website } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise { - return prisma.client.website.update({ - where: { - id: websiteId, - }, - data, - }); -} diff --git a/queries/index.js b/queries/index.js index 4cdcedd9..e14c6d84 100644 --- a/queries/index.js +++ b/queries/index.js @@ -1,21 +1,17 @@ -export * from './admin/user/createUser'; -export * from './admin/user/deleteUser'; -export * from './admin/user/getUser'; -export * from './admin/user/getUsers'; -export * from './admin/user/updateUser'; -export * from './admin/website/createWebsite'; -export * from './admin/website/deleteWebsite'; -export * from './admin/website/getAllWebsites'; -export * from './admin/website/getUserWebsites'; -export * from './admin/website/getWebsite'; -export * from './admin/website/resetWebsite'; -export * from './admin/website/updateWebsite'; +export * from './admin/permission'; +export * from './admin/role'; +export * from './admin/team'; +export * from './admin/teamUser'; +export * from './admin/teamWebsite'; +export * from './admin/user'; +export * from './admin/userRole'; +export * from './admin/userWebsite'; +export * from './admin/website'; export * from './analytics/event/getEventMetrics'; export * from './analytics/event/getEvents'; export * from './analytics/event/getEventData'; export * from './analytics/event/saveEvent'; export * from './analytics/pageview/getPageviewMetrics'; -export * from './analytics/pageview/getPageviewParams'; export * from './analytics/pageview/getPageviews'; export * from './analytics/pageview/getPageviewStats'; export * from './analytics/pageview/savePageView'; From f5eb974d8d958c800596ff5ae6dfffa8d6a1e3e8 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 17 Nov 2022 22:46:05 -0800 Subject: [PATCH 05/38] Fix Typo. --- pages/api/teams/[id]/index.ts | 87 ++++++++++++++++++++++++++++ pages/api/teams/index.ts | 0 pages/api/users/[id]/index.ts | 6 +- pages/api/websites/[id]/index.ts | 6 +- pages/api/websites/[id]/metrics.ts | 4 +- pages/api/websites/[id]/pageviews.ts | 4 +- pages/api/websites/[id]/reset.ts | 4 +- pages/api/websites/[id]/stats.ts | 4 +- pages/api/websites/index.ts | 6 +- 9 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 pages/api/teams/[id]/index.ts create mode 100644 pages/api/teams/index.ts diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts new file mode 100644 index 00000000..26440c8b --- /dev/null +++ b/pages/api/teams/[id]/index.ts @@ -0,0 +1,87 @@ +import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getTeam, deleteTeam, updateTeam } from 'queries'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { Team } from '@prisma/client'; + +export interface TeamRequestQuery { + id: string; +} + +export interface TeamRequestBody { + username: string; + password: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { + user: { id: userId, isAdmin }, + } = req.auth; + const { id } = req.query; + + if (req.method === 'GET') { + if (id !== userId && !isAdmin) { + return unauthorized(res); + } + + const user = await getTeam({ id }); + + return ok(res, user); + } + + if (req.method === 'POST') { + const { username, password } = req.body; + + if (id !== userId && !isAdmin) { + return unauthorized(res); + } + + const user = await getTeam({ id }); + + const data: any = {}; + + if (password) { + data.password = hashPassword(password); + } + + // Only admin can change these fields + if (isAdmin) { + data.username = username; + } + + // Check when username changes + if (data.username && user.username !== data.username) { + const userByTeamname = await getTeam({ username }); + + if (userByTeamname) { + return badRequest(res, 'Team already exists'); + } + } + + const updated = await updateTeam(data, { id }); + + return ok(res, updated); + } + + if (req.method === 'DELETE') { + if (id === userId) { + return badRequest(res, 'You cannot delete your own user.'); + } + + if (!isAdmin) { + return unauthorized(res); + } + + await deleteTeam(id); + + return ok(res); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index 870ff68a..80b6f8b2 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -5,17 +5,17 @@ import { NextApiResponse } from 'next'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { User } from 'interface/api/models'; -export interface UserReqeustQuery { +export interface UserRequestQuery { id: string; } -export interface UserReqeustBody { +export interface UserRequestBody { username: string; password: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 834b1732..2ec1ee14 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -7,18 +7,18 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiResponse } from 'next'; import { Website } from 'interface/api/models'; -export interface WebsiteReqeustQuery { +export interface WebsiteRequestQuery { id: string; } -export interface WebsiteReqeustBody { +export interface WebsiteRequestBody { name: string; domain: string; shareId: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index f3bf38aa..69c3c79d 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -36,7 +36,7 @@ function getColumn(type) { return type; } -export interface WebsiteMetricsReqeustQuery { +export interface WebsiteMetricsRequestQuery { id: string; type: string; start_at: number; @@ -50,7 +50,7 @@ export interface WebsiteMetricsReqeustQuery { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index c85a1a97..5bbf067b 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -10,7 +10,7 @@ import { getPageviewStats } from 'queries'; const unitTypes = ['year', 'month', 'hour', 'day']; -export interface WebsitePageviewReqeustQuery { +export interface WebsitePageviewRequestQuery { id: string; websiteId: string; start_at: number; @@ -26,7 +26,7 @@ export interface WebsitePageviewReqeustQuery { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index 6d2ffcea..ff141398 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -6,12 +6,12 @@ import { TYPE_WEBSITE } from 'lib/constants'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiResponse } from 'next'; -export interface WebsiteResetReqeustQuery { +export interface WebsiteResetRequestQuery { id: string; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index d9a581a9..497c3ff2 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -7,7 +7,7 @@ import { WebsiteStats } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiResponse } from 'next'; -export interface WebsiteStatsReqeustQuery { +export interface WebsiteStatsRequestQuery { id: string; type: string; start_at: number; @@ -21,7 +21,7 @@ export interface WebsiteStatsReqeustQuery { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index c4f2b3ea..4c5ad07c 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -5,18 +5,18 @@ import { NextApiResponse } from 'next'; import { getRandomChars, methodNotAllowed, ok } from 'next-basics'; import { createWebsiteByUser, getAllWebsites, getWebsitesByUserId } from 'queries'; -export interface WebsitesReqeustQuery { +export interface WebsitesRequestQuery { include_all?: boolean; } -export interface WebsitesReqeustBody { +export interface WebsitesRequestBody { name: string; domain: string; enableShareUrl: boolean; } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useCors(req, res); From 25279271ce84c3193739c0f297f984961cef0af6 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Nov 2022 00:27:42 -0800 Subject: [PATCH 06/38] Add some api/team endpoints. --- pages/api/teams/[id]/index.ts | 38 ++++++-------------------- pages/api/teams/[id]/user.ts | 48 +++++++++++++++++++++++++++++++++ pages/api/teams/[id]/website.ts | 48 +++++++++++++++++++++++++++++++++ pages/api/teams/index.ts | 47 ++++++++++++++++++++++++++++++++ pages/api/users/index.ts | 2 +- queries/admin/team.ts | 16 +++++------ queries/admin/teamUser.ts | 4 ++- queries/admin/teamWebsite.ts | 4 ++- queries/admin/userRole.ts | 4 ++- queries/admin/userWebsite.ts | 4 ++- 10 files changed, 172 insertions(+), 43 deletions(-) create mode 100644 pages/api/teams/[id]/user.ts create mode 100644 pages/api/teams/[id]/website.ts diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index 26440c8b..14710a54 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -1,17 +1,17 @@ -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getTeam, deleteTeam, updateTeam } from 'queries'; +import { Team } from '@prisma/client'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { Team } from '@prisma/client'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { deleteTeam, getTeam, updateTeam } from 'queries'; export interface TeamRequestQuery { id: string; } export interface TeamRequestBody { - username: string; - password: string; + name?: string; + is_deleted?: boolean; } export default async ( @@ -36,35 +36,13 @@ export default async ( } if (req.method === 'POST') { - const { username, password } = req.body; + const { name, is_deleted: isDeleted } = req.body; if (id !== userId && !isAdmin) { return unauthorized(res); } - const user = await getTeam({ id }); - - const data: any = {}; - - if (password) { - data.password = hashPassword(password); - } - - // Only admin can change these fields - if (isAdmin) { - data.username = username; - } - - // Check when username changes - if (data.username && user.username !== data.username) { - const userByTeamname = await getTeam({ username }); - - if (userByTeamname) { - return badRequest(res, 'Team already exists'); - } - } - - const updated = await updateTeam(data, { id }); + const updated = await updateTeam({ name, isDeleted }, { id }); return ok(res, updated); } diff --git a/pages/api/teams/[id]/user.ts b/pages/api/teams/[id]/user.ts new file mode 100644 index 00000000..9f1290e1 --- /dev/null +++ b/pages/api/teams/[id]/user.ts @@ -0,0 +1,48 @@ +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { uuid } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok } from 'next-basics'; +import { createTeamUser, deleteTeamUser, getUsersByTeamId } from 'queries'; + +export interface TeamUserRequestQuery { + id: string; +} + +export interface TeamUserRequestBody { + user_id: string; + team_user_id?: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { id: teamId } = req.query; + + if (req.method === 'GET') { + const user = await getUsersByTeamId({ teamId }); + + return ok(res, user); + } + + if (req.method === 'POST') { + const { user_id: userId } = req.body; + + const updated = await createTeamUser({ id: uuid(), userId, teamId }); + + return ok(res, updated); + } + + if (req.method === 'DELETE') { + const { team_user_id } = req.body; + + await deleteTeamUser(team_user_id); + + return ok(res); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/teams/[id]/website.ts b/pages/api/teams/[id]/website.ts new file mode 100644 index 00000000..35c2e36e --- /dev/null +++ b/pages/api/teams/[id]/website.ts @@ -0,0 +1,48 @@ +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { uuid } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok } from 'next-basics'; +import { createTeamWebsite, deleteTeamWebsite, getWebsitesByTeamId } from 'queries'; + +export interface TeamWebsiteRequestQuery { + id: string; +} + +export interface TeamWebsiteRequestBody { + website_id: string; + team_website_id?: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { id: teamId } = req.query; + + if (req.method === 'GET') { + const website = await getWebsitesByTeamId({ teamId }); + + return ok(res, website); + } + + if (req.method === 'POST') { + const { website_id: websiteId } = req.body; + + const updated = await createTeamWebsite({ id: uuid(), websiteId, teamId }); + + return ok(res, updated); + } + + if (req.method === 'DELETE') { + const { team_website_id } = req.body; + + await deleteTeamWebsite(team_website_id); + + return ok(res); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index e69de29b..8173c3b2 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -0,0 +1,47 @@ +import { Team } from '@prisma/client'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { uuid } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { badRequest, methodNotAllowed, ok } from 'next-basics'; +import { createTeam, getTeam, getTeamsByUserId } from 'queries'; +export interface TeamsRequestBody { + name: string; + description: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { + user: { id }, + } = req.auth; + + if (req.method === 'GET') { + const users = await getTeamsByUserId(id); + + return ok(res, users); + } + + if (req.method === 'POST') { + const { name } = req.body; + + const user = await getTeam({ name }); + + if (user) { + return badRequest(res, 'Team already exists'); + } + + const created = await createTeam({ + id: id || uuid(), + name, + }); + + return ok(res, created); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 6b942287..07007546 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -13,7 +13,7 @@ export interface UsersRequestBody { } export default async ( - req: NextApiRequestQueryBody, + req: NextApiRequestQueryBody, res: NextApiResponse, ) => { await useAuth(req, res); diff --git a/queries/admin/team.ts b/queries/admin/team.ts index 71b5f807..8687fb64 100644 --- a/queries/admin/team.ts +++ b/queries/admin/team.ts @@ -1,19 +1,19 @@ -import { Prisma, Role, Team, TeamUser } from '@prisma/client'; +import { Prisma, Team, TeamUser } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createTeam(data: Prisma.RoleCreateInput): Promise { +export async function createTeam(data: Prisma.TeamCreateInput): Promise { return prisma.client.role.create({ data, }); } -export async function getTeam(where: Prisma.RoleWhereUniqueInput): Promise { +export async function getTeam(where: Prisma.TeamWhereUniqueInput): Promise { return prisma.client.role.findUnique({ where, }); } -export async function getTeams(where: Prisma.RoleWhereInput): Promise { +export async function getTeams(where: Prisma.TeamWhereInput): Promise { return prisma.client.role.findMany({ where, }); @@ -35,16 +35,16 @@ export async function getTeamsByUserId(userId: string): Promise< } export async function updateTeam( - data: Prisma.RoleUpdateInput, - where: Prisma.RoleWhereUniqueInput, -): Promise { + data: Prisma.TeamUpdateInput, + where: Prisma.TeamWhereUniqueInput, +): Promise { return prisma.client.role.update({ data, where, }); } -export async function deleteTeam(teamId: string): Promise { +export async function deleteTeam(teamId: string): Promise { return prisma.client.role.update({ data: { isDeleted: true, diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts index e110efcf..b2f7bbf2 100644 --- a/queries/admin/teamUser.ts +++ b/queries/admin/teamUser.ts @@ -1,7 +1,9 @@ import { Prisma, TeamUser } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createTeamUser(data: Prisma.TeamUserCreateInput): Promise { +export async function createTeamUser( + data: Prisma.TeamUserCreateInput | Prisma.TeamUserUncheckedCreateInput, +): Promise { return prisma.client.teamUser.create({ data, }); diff --git a/queries/admin/teamWebsite.ts b/queries/admin/teamWebsite.ts index 950a7026..6b485da0 100644 --- a/queries/admin/teamWebsite.ts +++ b/queries/admin/teamWebsite.ts @@ -1,7 +1,9 @@ import { Prisma, TeamWebsite } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createTeamWebsite(data: Prisma.TeamWebsiteCreateInput): Promise { +export async function createTeamWebsite( + data: Prisma.TeamWebsiteCreateInput | Prisma.TeamWebsiteUncheckedCreateInput, +): Promise { return prisma.client.teamWebsite.create({ data, }); diff --git a/queries/admin/userRole.ts b/queries/admin/userRole.ts index c4e365ac..22893412 100644 --- a/queries/admin/userRole.ts +++ b/queries/admin/userRole.ts @@ -1,7 +1,9 @@ import { Prisma, UserRole } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createUserRole(data: Prisma.UserRoleCreateInput): Promise { +export async function createUserRole( + data: Prisma.UserRoleCreateInput | Prisma.UserRoleUncheckedCreateInput, +): Promise { return prisma.client.userRole.create({ data, }); diff --git a/queries/admin/userWebsite.ts b/queries/admin/userWebsite.ts index 313d6cd4..90039b8f 100644 --- a/queries/admin/userWebsite.ts +++ b/queries/admin/userWebsite.ts @@ -1,7 +1,9 @@ import { Prisma, UserWebsite } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createUserWebsite(data: Prisma.UserWebsiteCreateInput): Promise { +export async function createUserWebsite( + data: Prisma.UserWebsiteCreateInput | Prisma.UserWebsiteUncheckedCreateInput, +): Promise { return prisma.client.userWebsite.create({ data, }); From bba8876522cdb3ee7c42947df51052d82fa90fe0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 18 Nov 2022 03:20:45 -0800 Subject: [PATCH 07/38] Fix destructure error. --- hooks/useRequireLogin.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/hooks/useRequireLogin.js b/hooks/useRequireLogin.js index b2edc347..1e73f38b 100644 --- a/hooks/useRequireLogin.js +++ b/hooks/useRequireLogin.js @@ -12,17 +12,14 @@ export default function useRequireLogin() { async function loadUser() { setLoading(true); - const { - ok, - data: { user }, - } = await get('/auth/verify'); + const { ok, data } = await get('/auth/verify'); if (!ok) { await router.push('/login'); return null; } - setUser(user); + setUser(data.user); setLoading(false); } From 2b10802e07fd47f913232fbb05ab8cf365d90da0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 18 Nov 2022 09:47:06 -0800 Subject: [PATCH 08/38] Fix getWebsites call. --- pages/api/realtime/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/realtime/init.ts b/pages/api/realtime/init.ts index b1d1f32f..70aab0eb 100644 --- a/pages/api/realtime/init.ts +++ b/pages/api/realtime/init.ts @@ -13,7 +13,7 @@ export default async (req: NextApiRequestAuth, res: NextApiResponse id); const token = createToken({ websites: ids }, secret()); const data = await getRealtimeData(ids, subMinutes(new Date(), 30)); From 21072c712c6b56ef87b4c2258d23644a03d2baf7 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 18 Nov 2022 10:00:12 -0800 Subject: [PATCH 09/38] Ignore typescript build errors. --- next.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/next.config.js b/next.config.js index b95c39d4..a1dbb4e8 100644 --- a/next.config.js +++ b/next.config.js @@ -43,6 +43,9 @@ module.exports = { eslint: { ignoreDuringBuilds: true, }, + typescript: { + ignoreBuildErrors: true, + }, webpack(config) { config.module.rules.push({ test: /\.svg$/, From ce02958bc22a653749d9a967ed79a648cebfd45f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Nov 2022 11:22:03 -0800 Subject: [PATCH 10/38] Fix enum issue. --- interface/enum.d.ts => lib/enum.ts | 2 +- queries/admin/user.ts | 28 ++++++++----------- queries/analytics/event/getEventData.ts | 2 +- queries/analytics/event/getEventMetrics.ts | 2 +- queries/analytics/event/saveEvent.ts | 2 +- .../analytics/pageview/getPageviewMetrics.ts | 2 +- .../analytics/pageview/getPageviewStats.ts | 2 +- queries/analytics/pageview/savePageView.ts | 2 +- 8 files changed, 18 insertions(+), 24 deletions(-) rename interface/enum.d.ts => lib/enum.ts (79%) diff --git a/interface/enum.d.ts b/lib/enum.ts similarity index 79% rename from interface/enum.d.ts rename to lib/enum.ts index 8b09039c..d8270333 100644 --- a/interface/enum.d.ts +++ b/lib/enum.ts @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ export namespace UmamiApi { - enum EventType { + export enum EventType { Pageview = 1, Event = 2, } diff --git a/queries/admin/user.ts b/queries/admin/user.ts index 98eadc1b..e03974f2 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client'; -import { UmamiApi } from 'interface/enum'; +import { UmamiApi } from 'lib/enum'; import cache from 'lib/cache'; import prisma from 'lib/prisma'; @@ -82,22 +82,16 @@ export async function updateUser( data: Prisma.UserUpdateInput, where: Prisma.UserWhereUniqueInput, ): Promise { - return prisma.client.user - .update({ - where, - data, - select: { - id: true, - username: true, - createdAt: true, - userRole: true, - }, - }) - .then(user => { - const { userRole, ...rest } = user; - - return { ...rest, isAdmin: userRole.some(a => a.roleId === UmamiApi.SystemRole.Admin) }; - }); + return prisma.client.user.update({ + where, + data, + select: { + id: true, + username: true, + createdAt: true, + userRole: true, + }, + }); } export async function deleteUser( diff --git a/queries/analytics/event/getEventData.ts b/queries/analytics/event/getEventData.ts index 2e776f52..3c6bd460 100644 --- a/queries/analytics/event/getEventData.ts +++ b/queries/analytics/event/getEventData.ts @@ -3,7 +3,7 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; import { WebsiteMetric } from 'interface/api/models'; -import { UmamiApi } from 'interface/enum'; +import { UmamiApi } from 'lib/enum'; export async function getEventData( ...args: [ diff --git a/queries/analytics/event/getEventMetrics.ts b/queries/analytics/event/getEventMetrics.ts index c2e819c4..96843dce 100644 --- a/queries/analytics/event/getEventMetrics.ts +++ b/queries/analytics/event/getEventMetrics.ts @@ -3,7 +3,7 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; import { WebsiteEventMetric } from 'interface/api/models'; -import { UmamiApi } from 'interface/enum'; +import { UmamiApi } from 'lib/enum'; export async function getEventMetrics( ...args: [ diff --git a/queries/analytics/event/saveEvent.ts b/queries/analytics/event/saveEvent.ts index 6bb44ef5..02a349e3 100644 --- a/queries/analytics/event/saveEvent.ts +++ b/queries/analytics/event/saveEvent.ts @@ -4,7 +4,7 @@ import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import { uuid } from 'lib/crypto'; import cache from 'lib/cache'; -import { UmamiApi } from 'interface/enum'; +import { UmamiApi } from 'lib/enum'; export async function saveEvent(args: { id: string; diff --git a/queries/analytics/pageview/getPageviewMetrics.ts b/queries/analytics/pageview/getPageviewMetrics.ts index 106d14a9..de8fe448 100644 --- a/queries/analytics/pageview/getPageviewMetrics.ts +++ b/queries/analytics/pageview/getPageviewMetrics.ts @@ -3,7 +3,7 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; import { Prisma } from '@prisma/client'; -import { UmamiApi } from 'interface/enum'; +import { UmamiApi } from 'lib/enum'; export async function getPageviewMetrics( ...args: [ diff --git a/queries/analytics/pageview/getPageviewStats.ts b/queries/analytics/pageview/getPageviewStats.ts index 4a7a7782..f9cda5fe 100644 --- a/queries/analytics/pageview/getPageviewStats.ts +++ b/queries/analytics/pageview/getPageviewStats.ts @@ -2,7 +2,7 @@ import cache from 'lib/cache'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; -import { UmamiApi } from 'interface/enum'; +import { UmamiApi } from 'lib/enum'; export async function getPageviewStats( ...args: [ diff --git a/queries/analytics/pageview/savePageView.ts b/queries/analytics/pageview/savePageView.ts index 134dfd09..793a8cd4 100644 --- a/queries/analytics/pageview/savePageView.ts +++ b/queries/analytics/pageview/savePageView.ts @@ -4,7 +4,7 @@ import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; import { uuid } from 'lib/crypto'; -import { UmamiApi } from 'interface/enum'; +import { UmamiApi } from 'lib/enum'; export async function savePageView(args: { id: string; From eaa208652de637c6ec8aed17e66eb36d9f17e7a4 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Fri, 18 Nov 2022 12:08:13 -0800 Subject: [PATCH 11/38] add clickhouse route to deleteWebsite --- queries/admin/website.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/queries/admin/website.ts b/queries/admin/website.ts index 240798da..c88fb2db 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -1,6 +1,7 @@ import { Prisma, Website } from '@prisma/client'; import cache from 'lib/cache'; import prisma from 'lib/prisma'; +import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; export async function createWebsiteByUser( userId: string, @@ -150,7 +151,14 @@ export async function getAllWebsites(): Promise<(Website & { user: string })[]> export async function deleteWebsite( websiteId: string, -): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { +) { + return runQuery({ + [PRISMA]: () => deleteWebsiteRelationalQuery(websiteId), + [CLICKHOUSE]: () => deleteWebsiteClickhouseQuery(websiteId), + }); +} + +async function deleteWebsiteRelationalQuery(websiteId): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; return transaction([ @@ -174,3 +182,12 @@ export async function deleteWebsite( return data; }); } + +async function deleteWebsiteClickhouseQuery(websiteId): Promise { + return prisma.client.website.update({ + data: { + isDeleted: true, + }, + where: { id: websiteId }, + }); +} From e28ee6597ac3f9e248d4c26fb73aef2168ae2f3a Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Fri, 18 Nov 2022 18:49:58 -0800 Subject: [PATCH 12/38] Fix Website auth. --- interface/api/models.d.ts | 7 ---- lib/{auth.js => auth.ts} | 35 ++++++++++++------- lib/{cache.js => cache.ts} | 11 ++++-- lib/{constants.js => constants.ts} | 12 +++++++ lib/enum.ts | 7 ---- pages/api/auth/login.ts | 3 +- pages/api/users/[id]/password.ts | 17 +++++---- pages/api/websites/[id]/active.ts | 12 +++---- pages/api/websites/[id]/eventdata.ts | 25 +++++-------- pages/api/websites/[id]/events.ts | 4 +-- pages/api/websites/[id]/index.ts | 14 ++++---- pages/api/websites/[id]/metrics.ts | 4 +-- pages/api/websites/[id]/pageviews.ts | 4 +-- pages/api/websites/[id]/reset.ts | 12 +++---- pages/api/websites/[id]/stats.ts | 12 +++---- queries/admin/user.ts | 2 +- queries/admin/userWebsite.ts | 6 ++-- queries/analytics/event/getEventData.ts | 16 ++++----- queries/analytics/event/getEventMetrics.ts | 2 +- queries/analytics/event/saveEvent.ts | 2 +- .../analytics/pageview/getPageviewMetrics.ts | 2 +- .../analytics/pageview/getPageviewStats.ts | 2 +- queries/analytics/pageview/savePageView.ts | 2 +- 23 files changed, 108 insertions(+), 105 deletions(-) rename lib/{auth.js => auth.ts} (67%) rename lib/{cache.js => cache.ts} (80%) rename lib/{constants.js => constants.ts} (97%) delete mode 100644 lib/enum.ts diff --git a/interface/api/models.d.ts b/interface/api/models.d.ts index 6251a149..3359a651 100644 --- a/interface/api/models.d.ts +++ b/interface/api/models.d.ts @@ -1,10 +1,3 @@ -export interface User { - id: string; - username: string; - isAdmin: boolean; - createdAt: string; -} - export interface Website { id: string; userId: string; diff --git a/lib/auth.js b/lib/auth.ts similarity index 67% rename from lib/auth.js rename to lib/auth.ts index 09193e89..050e1c20 100644 --- a/lib/auth.js +++ b/lib/auth.ts @@ -1,8 +1,9 @@ -import { parseSecureToken, parseToken } from 'next-basics'; -import { getUser, getWebsite } from 'queries'; import debug from 'debug'; -import { SHARE_TOKEN_HEADER, TYPE_USER, TYPE_WEBSITE } from 'lib/constants'; +import { NextApiRequestAuth } from 'interface/api/nextApi'; +import { SHARE_TOKEN_HEADER, UmamiApi } from 'lib/constants'; import { secret } from 'lib/crypto'; +import { parseSecureToken, parseToken } from 'next-basics'; +import { getUser, getUserWebsite } from 'queries'; const log = debug('umami:auth'); @@ -47,30 +48,38 @@ export function isValidToken(token, validation) { return false; } -export async function allowQuery(req, type) { - const { id } = req.query; +export async function allowQuery( + req: NextApiRequestAuth, + type: UmamiApi.AuthType, + typeId?: string, +) { + const { id } = req.query as { id: string }; const { user, shareToken } = req.auth; - if (user?.isAdmin) { - return true; - } - if (shareToken) { return isValidToken(shareToken, { id }); } if (user?.id) { - if (type === TYPE_WEBSITE) { - const website = await getWebsite({ id }); + if (type === UmamiApi.AuthType.Website) { + const userWebsite = await getUserWebsite({ + userId: user.id, + websiteId: typeId ?? id, + isDeleted: false, + }); - return website && website.userId === user.id; - } else if (type === TYPE_USER) { + return userWebsite; + } else if (type === UmamiApi.AuthType.User) { const user = await getUser({ id }); return user && user.id === id; } } + if (user?.isAdmin) { + return true; + } + return false; } diff --git a/lib/cache.js b/lib/cache.ts similarity index 80% rename from lib/cache.js rename to lib/cache.ts index bc065c14..6ffbb5fd 100644 --- a/lib/cache.js +++ b/lib/cache.ts @@ -1,5 +1,6 @@ import { getWebsite, getUser, getSession } from '../queries'; import redis, { DELETED } from 'lib/redis'; +import { Role, Team, TeamUser, User, UserRole, UserWebsite, Website } from '@prisma/client'; async function fetchObject(key, query) { const obj = await redis.get(key); @@ -40,8 +41,14 @@ async function deleteWebsite(id) { return deleteObject(`website:${id}`); } -async function fetchUser(id) { - return fetchObject(`user:${id}`, () => getUser({ id })); +async function fetchUser(id): Promise< + User & { + userRole?: (UserRole & { role: Role })[]; + teamUser?: (TeamUser & { team: Team })[]; + userWebsite?: (UserWebsite & { website: Website })[]; + } +> { + return fetchObject(`user:${id}`, () => getUser({ id }, true)); } async function storeUser(data) { diff --git a/lib/constants.js b/lib/constants.ts similarity index 97% rename from lib/constants.js rename to lib/constants.ts index 0e4eabec..cf56c39f 100644 --- a/lib/constants.js +++ b/lib/constants.ts @@ -1,3 +1,15 @@ +/* eslint-disable no-unused-vars */ +export namespace UmamiApi { + export enum EventType { + Pageview = 1, + Event = 2, + } + + export enum AuthType { + Website, + User, + } +} export const CURRENT_VERSION = process.env.currentVersion; export const AUTH_TOKEN = 'umami.auth'; export const LOCALE_CONFIG = 'umami.locale'; diff --git a/lib/enum.ts b/lib/enum.ts deleted file mode 100644 index d8270333..00000000 --- a/lib/enum.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable no-unused-vars */ -export namespace UmamiApi { - export enum EventType { - Pageview = 1, - Event = 2, - } -} diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts index 321fb3ab..b7458a7a 100644 --- a/pages/api/auth/login.ts +++ b/pages/api/auth/login.ts @@ -7,12 +7,11 @@ import { methodNotAllowed, getRandomChars, } from 'next-basics'; -import { getUser } from 'queries'; +import { getUser, User } from 'queries'; import { secret } from 'lib/crypto'; import redis from 'lib/redis'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { NextApiResponse } from 'next'; -import { User } from 'interface/api/models'; export interface LoginRequestBody { username: string; diff --git a/pages/api/users/[id]/password.ts b/pages/api/users/[id]/password.ts index 4b00d7d5..e0024eda 100644 --- a/pages/api/users/[id]/password.ts +++ b/pages/api/users/[id]/password.ts @@ -1,18 +1,17 @@ -import { getUser, updateUser } from 'queries'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; import { badRequest, + checkPassword, + hashPassword, methodNotAllowed, ok, unauthorized, - checkPassword, - hashPassword, } from 'next-basics'; -import { allowQuery } from 'lib/auth'; -import { TYPE_USER } from 'lib/constants'; -import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { NextApiResponse } from 'next'; -import { User } from 'interface/api/models'; +import { getUser, updateUser, User } from 'queries'; export interface UserPasswordRequestQuery { id: string; @@ -32,7 +31,7 @@ export default async ( const { current_password, new_password } = req.body; const { id } = req.query; - if (!(await allowQuery(req, TYPE_USER))) { + if (!(await allowQuery(req, UmamiApi.AuthType.User))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/active.ts b/pages/api/websites/[id]/active.ts index b50c29e7..3323ad7b 100644 --- a/pages/api/websites/[id]/active.ts +++ b/pages/api/websites/[id]/active.ts @@ -1,11 +1,11 @@ -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { allowQuery } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; -import { getActiveVisitors } from 'queries'; -import { TYPE_WEBSITE } from 'lib/constants'; import { WebsiteActive } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; +import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getActiveVisitors } from 'queries'; export interface WebsiteActiveRequestQuery { id: string; @@ -19,7 +19,7 @@ export default async ( await useAuth(req, res); if (req.method === 'GET') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/eventdata.ts b/pages/api/websites/[id]/eventdata.ts index 646b2920..120f61cc 100644 --- a/pages/api/websites/[id]/eventdata.ts +++ b/pages/api/websites/[id]/eventdata.ts @@ -1,12 +1,11 @@ -import moment from 'moment-timezone'; -import { getEventData } from 'queries'; -import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics'; -import { allowQuery } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; -import { TYPE_WEBSITE } from 'lib/constants'; -import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { NextApiResponse } from 'next'; import { WebsiteMetric } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; +import { useAuth, useCors } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getEventData } from 'queries'; export interface WebsiteEventDataRequestQuery { id: string; @@ -15,7 +14,6 @@ export interface WebsiteEventDataRequestQuery { export interface WebsiteEventDataRequestBody { start_at: string; end_at: string; - timezone: string; event_name: string; columns: { [key: string]: 'count' | 'max' | 'min' | 'avg' | 'sum' }; filters?: { [key: string]: any }; @@ -29,17 +27,13 @@ export default async ( await useAuth(req, res); if (req.method === 'POST') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } const { id: websiteId } = req.query; - const { start_at, end_at, timezone, event_name: eventName, columns, filters } = req.body; - - if (!moment.tz.zone(timezone)) { - return badRequest(res); - } + const { start_at, end_at, event_name: eventName, columns, filters } = req.body; const startDate = new Date(+start_at); const endDate = new Date(+end_at); @@ -47,7 +41,6 @@ export default async ( const events = await getEventData(websiteId, { startDate, endDate, - timezone, eventName, columns, filters, diff --git a/pages/api/websites/[id]/events.ts b/pages/api/websites/[id]/events.ts index efbf5a39..832cb727 100644 --- a/pages/api/websites/[id]/events.ts +++ b/pages/api/websites/[id]/events.ts @@ -1,7 +1,7 @@ import { WebsiteMetric } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; -import { TYPE_WEBSITE } from 'lib/constants'; +import { UmamiApi } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; import moment from 'moment-timezone'; import { NextApiResponse } from 'next'; @@ -28,7 +28,7 @@ export default async ( await useAuth(req, res); if (req.method === 'GET') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 2ec1ee14..5cf6b474 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -1,11 +1,11 @@ +import { Website } from 'interface/api/models'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; +import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; -import { TYPE_WEBSITE } from 'lib/constants'; -import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { NextApiResponse } from 'next'; -import { Website } from 'interface/api/models'; export interface WebsiteRequestQuery { id: string; @@ -26,7 +26,7 @@ export default async ( const { id: websiteId } = req.query; - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } @@ -45,7 +45,7 @@ export default async ( domain, shareId, }); - } catch (e) { + } catch (e: any) { if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { return serverError(res, 'That share ID is already taken.'); } @@ -55,7 +55,7 @@ export default async ( } if (req.method === 'DELETE') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index 69c3c79d..d3cdbf07 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -1,7 +1,7 @@ import { WebsiteMetric } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; -import { FILTER_IGNORED, TYPE_WEBSITE } from 'lib/constants'; +import { FILTER_IGNORED, UmamiApi } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -57,7 +57,7 @@ export default async ( await useAuth(req, res); if (req.method === 'GET') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 5bbf067b..208a052a 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -1,7 +1,7 @@ import { WebsitePageviews } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; -import { TYPE_WEBSITE } from 'lib/constants'; +import { UmamiApi } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; import moment from 'moment-timezone'; import { NextApiResponse } from 'next'; @@ -33,7 +33,7 @@ export default async ( await useAuth(req, res); if (req.method === 'GET') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index ff141398..a5473ee6 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -1,10 +1,10 @@ -import { resetWebsite } from 'queries'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { allowQuery } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; -import { TYPE_WEBSITE } from 'lib/constants'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; +import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { resetWebsite } from 'queries'; export interface WebsiteResetRequestQuery { id: string; @@ -20,7 +20,7 @@ export default async ( const { id: websiteId } = req.query; if (req.method === 'POST') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index 497c3ff2..2122a2da 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -1,11 +1,11 @@ -import { getWebsiteStats } from 'queries'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { allowQuery } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; -import { TYPE_WEBSITE } from 'lib/constants'; import { WebsiteStats } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; +import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getWebsiteStats } from 'queries'; export interface WebsiteStatsRequestQuery { id: string; @@ -28,7 +28,7 @@ export default async ( await useAuth(req, res); if (req.method === 'GET') { - if (!(await allowQuery(req, TYPE_WEBSITE))) { + if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { return unauthorized(res); } diff --git a/queries/admin/user.ts b/queries/admin/user.ts index e03974f2..75a98194 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client'; -import { UmamiApi } from 'lib/enum'; +import { UmamiApi } from 'lib/constants'; import cache from 'lib/cache'; import prisma from 'lib/prisma'; diff --git a/queries/admin/userWebsite.ts b/queries/admin/userWebsite.ts index 90039b8f..76f924ec 100644 --- a/queries/admin/userWebsite.ts +++ b/queries/admin/userWebsite.ts @@ -9,10 +9,8 @@ export async function createUserWebsite( }); } -export async function getUserWebsite( - where: Prisma.UserWebsiteWhereUniqueInput, -): Promise { - return prisma.client.userWebsite.findUnique({ +export async function getUserWebsite(where: Prisma.UserWebsiteWhereInput): Promise { + return prisma.client.userWebsite.findFirst({ where, }); } diff --git a/queries/analytics/event/getEventData.ts b/queries/analytics/event/getEventData.ts index 3c6bd460..39002cf3 100644 --- a/queries/analytics/event/getEventData.ts +++ b/queries/analytics/event/getEventData.ts @@ -3,7 +3,7 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; import { WebsiteMetric } from 'interface/api/models'; -import { UmamiApi } from 'lib/enum'; +import { UmamiApi } from 'lib/constants'; export async function getEventData( ...args: [ @@ -11,7 +11,7 @@ export async function getEventData( data: { startDate: Date; endDate: Date; - event_name: string; + eventName: string; columns: any; filters: object; }, @@ -32,12 +32,12 @@ async function relationalQuery( data: { startDate: Date; endDate: Date; - event_name: string; + eventName: string; columns: any; filters: object; }, ) { - const { startDate, endDate, event_name, columns, filters } = data; + const { startDate, endDate, eventName, columns, filters } = data; const { rawQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma; const params = [startDate, endDate]; @@ -48,7 +48,7 @@ async function relationalQuery( where website_id ='${websiteId}' and created_at between $1 and $2 and event_type = ${UmamiApi.EventType.Event} - ${event_name ? `and event_name = ${event_name}` : ''} + ${eventName ? `and eventName = ${eventName}` : ''} ${ Object.keys(filters).length > 0 ? `and ${getEventDataFilterQuery('event_data', filters)}` @@ -63,12 +63,12 @@ async function clickhouseQuery( data: { startDate: Date; endDate: Date; - event_name: string; + eventName: string; columns: any; filters: object; }, ) { - const { startDate, endDate, event_name, columns, filters } = data; + const { startDate, endDate, eventName, columns, filters } = data; const { rawQuery, getBetweenDates, getEventDataColumnsQuery, getEventDataFilterQuery } = clickhouse; const website = await cache.fetchWebsite(websiteId); @@ -81,7 +81,7 @@ async function clickhouseQuery( where website_id = $1 and rev_id = $2 and event_type = ${UmamiApi.EventType.Event} - ${event_name ? `and event_name = ${event_name}` : ''} + ${eventName ? `and eventName = ${eventName}` : ''} and ${getBetweenDates('created_at', startDate, endDate)} ${ Object.keys(filters).length > 0 diff --git a/queries/analytics/event/getEventMetrics.ts b/queries/analytics/event/getEventMetrics.ts index 96843dce..ebf855f0 100644 --- a/queries/analytics/event/getEventMetrics.ts +++ b/queries/analytics/event/getEventMetrics.ts @@ -3,7 +3,7 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; import { WebsiteEventMetric } from 'interface/api/models'; -import { UmamiApi } from 'lib/enum'; +import { UmamiApi } from 'lib/constants'; export async function getEventMetrics( ...args: [ diff --git a/queries/analytics/event/saveEvent.ts b/queries/analytics/event/saveEvent.ts index 02a349e3..521f6c28 100644 --- a/queries/analytics/event/saveEvent.ts +++ b/queries/analytics/event/saveEvent.ts @@ -4,7 +4,7 @@ import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import { uuid } from 'lib/crypto'; import cache from 'lib/cache'; -import { UmamiApi } from 'lib/enum'; +import { UmamiApi } from 'lib/constants'; export async function saveEvent(args: { id: string; diff --git a/queries/analytics/pageview/getPageviewMetrics.ts b/queries/analytics/pageview/getPageviewMetrics.ts index de8fe448..f61e25bd 100644 --- a/queries/analytics/pageview/getPageviewMetrics.ts +++ b/queries/analytics/pageview/getPageviewMetrics.ts @@ -3,7 +3,7 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; import { Prisma } from '@prisma/client'; -import { UmamiApi } from 'lib/enum'; +import { UmamiApi } from 'lib/constants'; export async function getPageviewMetrics( ...args: [ diff --git a/queries/analytics/pageview/getPageviewStats.ts b/queries/analytics/pageview/getPageviewStats.ts index f9cda5fe..212a07de 100644 --- a/queries/analytics/pageview/getPageviewStats.ts +++ b/queries/analytics/pageview/getPageviewStats.ts @@ -2,7 +2,7 @@ import cache from 'lib/cache'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; -import { UmamiApi } from 'lib/enum'; +import { UmamiApi } from 'lib/constants'; export async function getPageviewStats( ...args: [ diff --git a/queries/analytics/pageview/savePageView.ts b/queries/analytics/pageview/savePageView.ts index 793a8cd4..fd3f7e81 100644 --- a/queries/analytics/pageview/savePageView.ts +++ b/queries/analytics/pageview/savePageView.ts @@ -4,7 +4,7 @@ import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; import { uuid } from 'lib/crypto'; -import { UmamiApi } from 'lib/enum'; +import { UmamiApi } from 'lib/constants'; export async function savePageView(args: { id: string; From 817880b511926628fdcdf259c886ba730125bec6 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 19 Nov 2022 18:28:46 -0800 Subject: [PATCH 13/38] Updated lint-staged config. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3b4304f..320283c4 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "postbuild": "node scripts/postbuild.js" }, "lint-staged": { - "**/*.js": [ + "**/*.{js,jsx,ts,tsx}": [ "prettier --write", "eslint" ], From 78225691df03db27d7706beb873d8f7c6b786b2d Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sun, 20 Nov 2022 00:48:13 -0800 Subject: [PATCH 14/38] Add permission checks. --- db/postgresql/schema.prisma | 55 ++++++------------- lib/auth.ts | 51 ++++++++++++++---- lib/cache.ts | 14 ++--- lib/constants.ts | 10 ++++ pages/api/teams/[id]/index.ts | 35 +++++------- pages/api/teams/[id]/user.ts | 16 +++++- pages/api/teams/[id]/website.ts | 27 +++------- pages/api/teams/index.ts | 8 +-- pages/api/users/[id]/index.ts | 19 +++---- pages/api/users/index.ts | 17 +++--- pages/api/websites/[id]/index.ts | 20 ++++--- pages/api/websites/index.ts | 26 ++++++--- queries/admin/permission.ts | 21 ++++++++ queries/admin/team.ts | 47 +++++++++-------- queries/admin/teamUser.ts | 4 +- queries/admin/teamWebsite.ts | 45 ---------------- queries/admin/user.ts | 5 +- queries/admin/userRole.ts | 4 +- queries/admin/userWebsite.ts | 43 --------------- queries/admin/website.ts | 91 ++++++-------------------------- 20 files changed, 225 insertions(+), 333 deletions(-) delete mode 100644 queries/admin/teamWebsite.ts delete mode 100644 queries/admin/userWebsite.ts diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 538a1fa8..d47b9d55 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -14,12 +14,12 @@ model User { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - groupRole GroupRole[] - groupUser GroupUser[] - userRole UserRole[] - teamWebsite TeamWebsite[] - teamUser TeamUser[] - userWebsite UserWebsite[] + groupRole GroupRole[] + groupUser GroupUser[] + userRole UserRole[] + teamUser TeamUser[] + Website Website? @relation(fields: [websiteId], references: [id]) + websiteId String? @db.Uuid @@map("user") } @@ -47,11 +47,13 @@ model Website { domain String? @db.VarChar(500) shareId String? @unique @map("share_id") @db.VarChar(64) revId Int @default(0) @map("rev_id") @db.Integer + userId String? @map("user_id") @db.Uuid + teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - teamWebsite TeamWebsite[] - userWebsite UserWebsite[] + team Team[] + user User[] @@index([createdAt]) @@index([shareId]) @@ -153,6 +155,7 @@ model RolePermission { role Role @relation(fields: [roleId], references: [id]) permission Permission @relation(fields: [permissionId], references: [id]) + @@unique([roleId, permissionId]) @@map("role_permission") } @@ -168,6 +171,7 @@ model UserRole { user User @relation(fields: [userId], references: [id]) team Team? @relation(fields: [teamId], references: [id]) + @@unique([roleId, userId, teamId]) @@map("user_role") } @@ -177,28 +181,14 @@ model Team { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - teamWebsites TeamWebsite[] - teamUsers TeamUser[] - UserRole UserRole[] + teamUsers TeamUser[] + UserRole UserRole[] + Website Website? @relation(fields: [websiteId], references: [id]) + websiteId String? @db.Uuid @@map("team") } -model TeamWebsite { - id String @id() @unique() @map("team_website_id") @db.Uuid - teamId String @map("team_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") - - website Website @relation(fields: [websiteId], references: [id]) - team Team @relation(fields: [teamId], references: [id]) - user User? @relation(fields: [userId], references: [id]) - userId String? @db.Uuid - - @@map("team_website") -} - model TeamUser { id String @id() @unique() @map("team_user_id") @db.Uuid teamId String @map("team_id") @db.Uuid @@ -212,16 +202,3 @@ model TeamUser { @@map("team_user") } - -model UserWebsite { - id String @id() @unique() @map("user_website_id") @db.Uuid - userId String @map("user_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") - - website Website @relation(fields: [websiteId], references: [id]) - user User @relation(fields: [userId], references: [id]) - - @@map("user_website") -} diff --git a/lib/auth.ts b/lib/auth.ts index 050e1c20..7035df69 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,9 +1,10 @@ import debug from 'debug'; import { NextApiRequestAuth } from 'interface/api/nextApi'; +import cache from 'lib/cache'; import { SHARE_TOKEN_HEADER, UmamiApi } from 'lib/constants'; import { secret } from 'lib/crypto'; import { parseSecureToken, parseToken } from 'next-basics'; -import { getUser, getUserWebsite } from 'queries'; +import { getPermissionsByUserId, getTeamUser, getUser } from 'queries'; const log = debug('umami:auth'); @@ -63,23 +64,51 @@ export async function allowQuery( if (user?.id) { if (type === UmamiApi.AuthType.Website) { - const userWebsite = await getUserWebsite({ - userId: user.id, - websiteId: typeId ?? id, - isDeleted: false, - }); + const website = await cache.fetchWebsite(typeId ?? id); - return userWebsite; + if (website && website.userId === user.id) { + return true; + } + + if (website.teamId) { + const teamUser = getTeamUser({ userId: user.id, teamId: website.teamId, isDeleted: false }); + + return teamUser; + } + + return false; } else if (type === UmamiApi.AuthType.User) { const user = await getUser({ id }); return user && user.id === id; - } - } + } else if (type === UmamiApi.AuthType.Team) { + const teamUser = await getTeamUser({ + userId: user.id, + teamId: typeId ?? id, + isDeleted: false, + }); - if (user?.isAdmin) { - return true; + return teamUser; + } else if (type === UmamiApi.AuthType.TeamOwner) { + const teamUser = await getTeamUser({ + userId: user.id, + teamId: typeId ?? id, + isDeleted: false, + }); + + return teamUser && teamUser.isOwner; + } } return false; } + +export async function checkPermission(req: NextApiRequestAuth, type: UmamiApi.Permission) { + const { + user: { id: userId }, + } = req.auth; + + const userRole = await getPermissionsByUserId(userId, type); + + return userRole.length > 0; +} diff --git a/lib/cache.ts b/lib/cache.ts index 6ffbb5fd..0cf6e2c8 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,6 +1,6 @@ -import { getWebsite, getUser, getSession } from '../queries'; +import { User, Website } from '@prisma/client'; import redis, { DELETED } from 'lib/redis'; -import { Role, Team, TeamUser, User, UserRole, UserWebsite, Website } from '@prisma/client'; +import { getSession, getUser, getWebsite } from '../queries'; async function fetchObject(key, query) { const obj = await redis.get(key); @@ -26,7 +26,7 @@ async function deleteObject(key) { return redis.set(key, DELETED); } -async function fetchWebsite(id) { +async function fetchWebsite(id): Promise { return fetchObject(`website:${id}`, () => getWebsite({ id })); } @@ -41,13 +41,7 @@ async function deleteWebsite(id) { return deleteObject(`website:${id}`); } -async function fetchUser(id): Promise< - User & { - userRole?: (UserRole & { role: Role })[]; - teamUser?: (TeamUser & { team: Team })[]; - userWebsite?: (UserWebsite & { website: Website })[]; - } -> { +async function fetchUser(id): Promise { return fetchObject(`user:${id}`, () => getUser({ id }, true)); } diff --git a/lib/constants.ts b/lib/constants.ts index cf56c39f..dbdf4e38 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -8,6 +8,16 @@ export namespace UmamiApi { export enum AuthType { Website, User, + Team, + TeamOwner, + } + + export enum Permission { + Admin = 'Admin', + } + + export enum Role { + Admin = 'Admin', } } export const CURRENT_VERSION = process.env.currentVersion; diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index 14710a54..e93621eb 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -1,8 +1,10 @@ import { Team } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { deleteTeam, getTeam, updateTeam } from 'queries'; export interface TeamRequestQuery { @@ -10,8 +12,7 @@ export interface TeamRequestQuery { } export interface TeamRequestBody { - name?: string; - is_deleted?: boolean; + name: string; } export default async ( @@ -20,43 +21,35 @@ export default async ( ) => { await useAuth(req, res); - const { - user: { id: userId, isAdmin }, - } = req.auth; - const { id } = req.query; + const { id: teamId } = req.query; if (req.method === 'GET') { - if (id !== userId && !isAdmin) { + if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { return unauthorized(res); } - - const user = await getTeam({ id }); + const user = await getTeam({ id: teamId }); return ok(res, user); } if (req.method === 'POST') { - const { name, is_deleted: isDeleted } = req.body; + const { name } = req.body; - if (id !== userId && !isAdmin) { - return unauthorized(res); + if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { + return unauthorized(res, 'You must be the owner of this team.'); } - const updated = await updateTeam({ name, isDeleted }, { id }); + const updated = await updateTeam({ name }, { id: teamId }); return ok(res, updated); } if (req.method === 'DELETE') { - if (id === userId) { - return badRequest(res, 'You cannot delete your own user.'); + if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { + return unauthorized(res, 'You must be the owner of this team.'); } - if (!isAdmin) { - return unauthorized(res); - } - - await deleteTeam(id); + await deleteTeam(teamId); return ok(res); } diff --git a/pages/api/teams/[id]/user.ts b/pages/api/teams/[id]/user.ts index 9f1290e1..529f0195 100644 --- a/pages/api/teams/[id]/user.ts +++ b/pages/api/teams/[id]/user.ts @@ -1,8 +1,10 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createTeamUser, deleteTeamUser, getUsersByTeamId } from 'queries'; export interface TeamUserRequestQuery { @@ -23,12 +25,20 @@ export default async ( const { id: teamId } = req.query; if (req.method === 'GET') { + if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { + return unauthorized(res); + } + const user = await getUsersByTeamId({ teamId }); return ok(res, user); } if (req.method === 'POST') { + if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { + return unauthorized(res, 'You must be the owner of this team.'); + } + const { user_id: userId } = req.body; const updated = await createTeamUser({ id: uuid(), userId, teamId }); @@ -37,6 +47,10 @@ export default async ( } if (req.method === 'DELETE') { + if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { + return unauthorized(res, 'You must be the owner of this team.'); + } + const { team_user_id } = req.body; await deleteTeamUser(team_user_id); diff --git a/pages/api/teams/[id]/website.ts b/pages/api/teams/[id]/website.ts index 35c2e36e..364fc5da 100644 --- a/pages/api/teams/[id]/website.ts +++ b/pages/api/teams/[id]/website.ts @@ -1,9 +1,10 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { uuid } from 'lib/crypto'; +import { allowQuery } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok } from 'next-basics'; -import { createTeamWebsite, deleteTeamWebsite, getWebsitesByTeamId } from 'queries'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { getWebsitesByTeamId } from 'queries'; export interface TeamWebsiteRequestQuery { id: string; @@ -23,26 +24,14 @@ export default async ( const { id: teamId } = req.query; if (req.method === 'GET') { + if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { + return unauthorized(res); + } + const website = await getWebsitesByTeamId({ teamId }); return ok(res, website); } - if (req.method === 'POST') { - const { website_id: websiteId } = req.body; - - const updated = await createTeamWebsite({ id: uuid(), websiteId, teamId }); - - return ok(res, updated); - } - - if (req.method === 'DELETE') { - const { team_website_id } = req.body; - - await deleteTeamWebsite(team_website_id); - - return ok(res); - } - return methodNotAllowed(res); }; diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 8173c3b2..5e8f6f4f 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -21,17 +21,17 @@ export default async ( } = req.auth; if (req.method === 'GET') { - const users = await getTeamsByUserId(id); + const teams = await getTeamsByUserId(id); - return ok(res, users); + return ok(res, teams); } if (req.method === 'POST') { const { name } = req.body; - const user = await getTeam({ name }); + const team = await getTeam({ name }); - if (user) { + if (team) { return badRequest(res, 'Team already exists'); } diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index 80b6f8b2..8eef949b 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -1,9 +1,10 @@ -import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getUser, deleteUser, updateUser } from 'queries'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { checkPermission } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { User } from 'interface/api/models'; +import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { deleteUser, getUser, updateUser, User } from 'queries'; export interface UserRequestQuery { id: string; @@ -21,12 +22,12 @@ export default async ( await useAuth(req, res); const { - user: { id: userId, isAdmin }, + user: { id: userId }, } = req.auth; const { id } = req.query; if (req.method === 'GET') { - if (id !== userId && !isAdmin) { + if (id !== userId) { return unauthorized(res); } @@ -38,7 +39,7 @@ export default async ( if (req.method === 'POST') { const { username, password } = req.body; - if (id !== userId && !isAdmin) { + if (id !== userId) { return unauthorized(res); } @@ -51,7 +52,7 @@ export default async ( } // Only admin can change these fields - if (isAdmin) { + if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { data.username = username; } @@ -74,7 +75,7 @@ export default async ( return badRequest(res, 'You cannot delete your own user.'); } - if (!isAdmin) { + if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { return unauthorized(res); } diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 07007546..5f15c44a 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,10 +1,11 @@ -import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'next-basics'; -import { useAuth } from 'lib/middleware'; -import { uuid } from 'lib/crypto'; -import { createUser, getUser, getUsers } from 'queries'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { checkPermission } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; +import { uuid } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { User } from 'interface/api/models'; +import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { createUser, getUser, getUsers, User } from 'queries'; export interface UsersRequestBody { username: string; @@ -18,11 +19,7 @@ export default async ( ) => { await useAuth(req, res); - const { - user: { isAdmin }, - } = req.auth; - - if (!isAdmin) { + if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 5cf6b474..f26313c4 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -4,7 +4,7 @@ import { allowQuery } from 'lib/auth'; import { UmamiApi } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; +import { methodNotAllowed, ok, serverError, unauthorized, badRequest } from 'next-basics'; import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; export interface WebsiteRequestQuery { @@ -15,6 +15,8 @@ export interface WebsiteRequestBody { name: string; domain: string; shareId: string; + userId?: string; + teamId?: string; } export default async ( @@ -37,14 +39,14 @@ export default async ( } if (req.method === 'POST') { - const { name, domain, shareId } = req.body; + const { ...data } = req.body; + + if (!data.userId && !data.teamId) { + badRequest(res, 'A website must be assigned to a User or Team.'); + } try { - await updateWebsite(websiteId, { - name, - domain, - shareId, - }); + await updateWebsite(websiteId, data); } catch (e: any) { if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { return serverError(res, 'That share ID is already taken.'); @@ -55,10 +57,6 @@ export default async ( } if (req.method === 'DELETE') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { - return unauthorized(res); - } - await deleteWebsite(websiteId); return ok(res); diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index 4c5ad07c..50469efb 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,9 +1,10 @@ +import { Prisma } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { uuid } from 'lib/crypto'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { getRandomChars, methodNotAllowed, ok } from 'next-basics'; -import { createWebsiteByUser, getAllWebsites, getWebsitesByUserId } from 'queries'; +import { methodNotAllowed, ok } from 'next-basics'; +import { createWebsite, getAllWebsites, getWebsitesByUserId } from 'queries'; export interface WebsitesRequestQuery { include_all?: boolean; @@ -12,7 +13,8 @@ export interface WebsitesRequestQuery { export interface WebsitesRequestBody { name: string; domain: string; - enableShareUrl: boolean; + shareId: string; + teamId?: string; } export default async ( @@ -36,10 +38,22 @@ export default async ( } if (req.method === 'POST') { - const { name, domain, enableShareUrl } = req.body; + const { name, domain, shareId, teamId } = req.body; - const shareId = enableShareUrl ? getRandomChars(8) : null; - const website = await createWebsiteByUser(userId, { id: uuid(), name, domain, shareId }); + const data: Prisma.WebsiteCreateInput = { + id: uuid(), + name, + domain, + shareId, + }; + + if (teamId) { + data.teamId = teamId; + } else { + data.userId = userId; + } + + const website = await createWebsite(data); return ok(res, website); } diff --git a/queries/admin/permission.ts b/queries/admin/permission.ts index 37d1647e..9c98065c 100644 --- a/queries/admin/permission.ts +++ b/queries/admin/permission.ts @@ -19,6 +19,27 @@ export async function getPermissions(where: Prisma.PermissionWhereInput): Promis }); } +export async function getPermissionsByUserId(userId, name?: string): Promise { + return prisma.client.permission.findMany({ + where: { + ...(name ? { name } : {}), + RolePermission: { + every: { + role: { + is: { + userRoles: { + every: { + userId, + }, + }, + }, + }, + }, + }, + }, + }); +} + export async function updatePermission( data: Prisma.PermissionUpdateInput, where: Prisma.PermissionWhereUniqueInput, diff --git a/queries/admin/team.ts b/queries/admin/team.ts index 8687fb64..2ea81e0a 100644 --- a/queries/admin/team.ts +++ b/queries/admin/team.ts @@ -1,51 +1,54 @@ -import { Prisma, Team, TeamUser } from '@prisma/client'; +import { Prisma, Team } from '@prisma/client'; import prisma from 'lib/prisma'; -export async function createTeam(data: Prisma.TeamCreateInput): Promise { - return prisma.client.role.create({ - data, +export async function createTeam( + data: Prisma.TeamCreateInput, + searchDeleted = false, +): Promise { + return prisma.client.team.create({ + data: { ...data, isDeleted: searchDeleted ? null : false }, }); } -export async function getTeam(where: Prisma.TeamWhereUniqueInput): Promise { - return prisma.client.role.findUnique({ +export async function getTeam(where: Prisma.TeamWhereInput): Promise { + return prisma.client.team.findFirst({ where, }); } export async function getTeams(where: Prisma.TeamWhereInput): Promise { - return prisma.client.role.findMany({ + return prisma.client.team.findMany({ where, }); } -export async function getTeamsByUserId(userId: string): Promise< - (TeamUser & { - team: Team; - })[] -> { - return prisma.client.teamUser.findMany({ - where: { - userId, - }, - include: { - team: true, - }, - }); +export async function getTeamsByUserId(userId: string): Promise { + return prisma.client.teamUser + .findMany({ + where: { + userId, + }, + include: { + team: true, + }, + }) + .then(data => { + return data.map(a => a.team); + }); } export async function updateTeam( data: Prisma.TeamUpdateInput, where: Prisma.TeamWhereUniqueInput, ): Promise { - return prisma.client.role.update({ + return prisma.client.team.update({ data, where, }); } export async function deleteTeam(teamId: string): Promise { - return prisma.client.role.update({ + return prisma.client.team.update({ data: { isDeleted: true, }, diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts index b2f7bbf2..3bbe9a76 100644 --- a/queries/admin/teamUser.ts +++ b/queries/admin/teamUser.ts @@ -9,8 +9,8 @@ export async function createTeamUser( }); } -export async function getTeamUser(where: Prisma.TeamUserWhereUniqueInput): Promise { - return prisma.client.teamUser.findUnique({ +export async function getTeamUser(where: Prisma.TeamUserWhereInput): Promise { + return prisma.client.teamUser.findFirst({ where, }); } diff --git a/queries/admin/teamWebsite.ts b/queries/admin/teamWebsite.ts deleted file mode 100644 index 6b485da0..00000000 --- a/queries/admin/teamWebsite.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Prisma, TeamWebsite } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function createTeamWebsite( - data: Prisma.TeamWebsiteCreateInput | Prisma.TeamWebsiteUncheckedCreateInput, -): Promise { - return prisma.client.teamWebsite.create({ - data, - }); -} - -export async function getTeamWebsite( - where: Prisma.TeamWebsiteWhereUniqueInput, -): Promise { - return prisma.client.teamWebsite.findUnique({ - where, - }); -} - -export async function getTeamWebsites(where: Prisma.TeamWebsiteWhereInput): Promise { - return prisma.client.teamWebsite.findMany({ - where, - }); -} - -export async function updateTeamWebsite( - data: Prisma.TeamWebsiteUpdateInput, - where: Prisma.TeamWebsiteWhereUniqueInput, -): Promise { - return prisma.client.teamWebsite.update({ - data, - where, - }); -} - -export async function deleteTeamWebsite(teamWebsiteId: string): Promise { - return prisma.client.teamWebsite.update({ - data: { - isDeleted: true, - }, - where: { - id: teamWebsiteId, - }, - }); -} diff --git a/queries/admin/user.ts b/queries/admin/user.ts index 75a98194..7122323b 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -1,5 +1,4 @@ import { Prisma } from '@prisma/client'; -import { UmamiApi } from 'lib/constants'; import cache from 'lib/cache'; import prisma from 'lib/prisma'; @@ -99,14 +98,14 @@ export async function deleteUser( ): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Prisma.BatchPayload, User]> { const { client } = prisma; - const websites = await client.userWebsite.findMany({ + const websites = await client.website.findMany({ where: { userId }, }); let websiteIds = []; if (websites.length > 0) { - websiteIds = websites.map(a => a.websiteId); + websiteIds = websites.map(a => a.id); } return client diff --git a/queries/admin/userRole.ts b/queries/admin/userRole.ts index 22893412..ee2a1a2f 100644 --- a/queries/admin/userRole.ts +++ b/queries/admin/userRole.ts @@ -9,8 +9,8 @@ export async function createUserRole( }); } -export async function getUserRole(where: Prisma.UserRoleWhereUniqueInput): Promise { - return prisma.client.userRole.findUnique({ +export async function getUserRole(where: Prisma.UserRoleWhereInput): Promise { + return prisma.client.userRole.findFirst({ where, }); } diff --git a/queries/admin/userWebsite.ts b/queries/admin/userWebsite.ts deleted file mode 100644 index 76f924ec..00000000 --- a/queries/admin/userWebsite.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Prisma, UserWebsite } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function createUserWebsite( - data: Prisma.UserWebsiteCreateInput | Prisma.UserWebsiteUncheckedCreateInput, -): Promise { - return prisma.client.userWebsite.create({ - data, - }); -} - -export async function getUserWebsite(where: Prisma.UserWebsiteWhereInput): Promise { - return prisma.client.userWebsite.findFirst({ - where, - }); -} - -export async function getUserWebsites(where: Prisma.UserWebsiteWhereInput): Promise { - return prisma.client.userWebsite.findMany({ - where, - }); -} - -export async function updateUserWebsite( - data: Prisma.UserWebsiteUpdateInput, - where: Prisma.UserWebsiteWhereUniqueInput, -): Promise { - return prisma.client.userWebsite.update({ - data, - where, - }); -} - -export async function deleteUserWebsite(userWebsiteId: string): Promise { - return prisma.client.userWebsite.update({ - data: { - isDeleted: true, - }, - where: { - id: userWebsiteId, - }, - }); -} diff --git a/queries/admin/website.ts b/queries/admin/website.ts index c88fb2db..7ee64a35 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -3,54 +3,12 @@ import cache from 'lib/cache'; import prisma from 'lib/prisma'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; -export async function createWebsiteByUser( - userId: string, - data: { - id: string; - name: string; - domain: string; - shareId?: string; - }, +export async function createWebsite( + data: Prisma.WebsiteCreateInput | Prisma.WebsiteUncheckedCreateInput, ): Promise { return prisma.client.website .create({ - data: { - userWebsite: { - connect: { - id: userId, - }, - }, - ...data, - }, - }) - .then(async data => { - if (cache.enabled) { - await cache.storeWebsite(data); - } - - return data; - }); -} - -export async function createWebsiteByTeam( - teamId: string, - data: { - id: string; - name: string; - domain: string; - shareId?: string; - }, -): Promise { - return prisma.client.website - .create({ - data: { - teamWebsite: { - connect: { - id: teamId, - }, - }, - ...data, - }, + data, }) .then(async data => { if (cache.enabled) { @@ -103,11 +61,7 @@ export async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise export async function getWebsitesByUserId(userId): Promise { return prisma.client.website.findMany({ where: { - userWebsite: { - every: { - userId, - }, - }, + userId, }, orderBy: { name: 'asc', @@ -118,11 +72,7 @@ export async function getWebsitesByUserId(userId): Promise { export async function getWebsitesByTeamId(teamId): Promise { return prisma.client.website.findMany({ where: { - teamWebsite: { - every: { - teamId, - }, - }, + teamId, }, orderBy: { name: 'asc', @@ -130,35 +80,26 @@ export async function getWebsitesByTeamId(teamId): Promise { }); } -export async function getAllWebsites(): Promise<(Website & { user: string })[]> { - return await prisma.client.website - .findMany({ - orderBy: [ - { - name: 'asc', - }, - ], - include: { - userWebsite: { - include: { - user: true, - }, - }, +export async function getAllWebsites(): Promise { + return await prisma.client.website.findMany({ + orderBy: [ + { + name: 'asc', }, - }) - .then(data => data.map(i => ({ ...i, user: i.userWebsite[0]?.userId }))); + ], + }); } -export async function deleteWebsite( - websiteId: string, -) { +export async function deleteWebsite(websiteId: string) { return runQuery({ [PRISMA]: () => deleteWebsiteRelationalQuery(websiteId), [CLICKHOUSE]: () => deleteWebsiteClickhouseQuery(websiteId), }); } -async function deleteWebsiteRelationalQuery(websiteId): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { +async function deleteWebsiteRelationalQuery( + websiteId, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; return transaction([ From 8fc6e99aa8cbcd72144b4c325a2e33a9222e1fa3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Sun, 20 Nov 2022 22:24:31 -0800 Subject: [PATCH 15/38] Add user role api. --- pages/api/users/[id]/role.ts | 73 ++++++++++++++++++++++++++++++++++++ queries/admin/userRole.ts | 9 +++++ queries/index.js | 2 - 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 pages/api/users/[id]/role.ts diff --git a/pages/api/users/[id]/role.ts b/pages/api/users/[id]/role.ts new file mode 100644 index 00000000..aab90ebd --- /dev/null +++ b/pages/api/users/[id]/role.ts @@ -0,0 +1,73 @@ +import { UserRole } from '@prisma/client'; +import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { checkPermission } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; +import { uuid } from 'lib/crypto'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { createUserRole, deleteUserRole, getUserRole, getUserRoles } from 'queries'; + +export interface UserRoleRequestQuery { + id: string; +} + +export interface UserRoleRequestBody { + roleId: string; + teamId?: string; + userRoleId?: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useAuth(req, res); + + const { + user: { id: userId }, + } = req.auth; + const { id } = req.query; + + if (id !== userId || !(await checkPermission(req, UmamiApi.Permission.Admin))) { + return unauthorized(res); + } + + if (req.method === 'GET') { + const userRole = await getUserRoles({ userId: id }); + + return ok(res, userRole); + } + + if (req.method === 'POST') { + const { roleId, teamId } = req.body; + + // Check when userRolename changes + const userRole = getUserRole({ userId: id, roleId, teamId }); + + if (userRole) { + return badRequest(res, 'Role already exists for User.'); + } + + const updated = await createUserRole({ id: uuid(), userId: id, roleId, teamId }); + + return ok(res, updated); + } + + if (req.method === 'DELETE') { + const { userRoleId } = req.body; + + // Check when userRolename changes + const userRole = getUserRole({ id: userRoleId }); + + if (userRole) { + return badRequest(res, 'Role already exists for User.'); + } + + const updated = await deleteUserRole(userRoleId); + + return ok(res, updated); + } + + return methodNotAllowed(res); +}; diff --git a/queries/admin/userRole.ts b/queries/admin/userRole.ts index ee2a1a2f..b93b1042 100644 --- a/queries/admin/userRole.ts +++ b/queries/admin/userRole.ts @@ -21,6 +21,15 @@ export async function getUserRoles(where: Prisma.UserRoleWhereInput): Promise { + return prisma.client.userRole.findMany({ + where: { + userId, + teamId, + }, + }); +} + export async function updateUserRole( data: Prisma.UserRoleUpdateInput, where: Prisma.UserRoleWhereUniqueInput, diff --git a/queries/index.js b/queries/index.js index e14c6d84..1a029edb 100644 --- a/queries/index.js +++ b/queries/index.js @@ -2,10 +2,8 @@ export * from './admin/permission'; export * from './admin/role'; export * from './admin/team'; export * from './admin/teamUser'; -export * from './admin/teamWebsite'; export * from './admin/user'; export * from './admin/userRole'; -export * from './admin/userWebsite'; export * from './admin/website'; export * from './analytics/event/getEventMetrics'; export * from './analytics/event/getEvents'; From 371425ab60ee19b723a0dda43d4cb3b9924d7d5c Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 21 Nov 2022 11:39:59 -0800 Subject: [PATCH 16/38] Fix error when updating website. --- pages/api/websites/[id]/index.ts | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index f26313c4..7b634e13 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -41,7 +41,7 @@ export default async ( if (req.method === 'POST') { const { ...data } = req.body; - if (!data.userId && !data.teamId) { + if (data.userId && data.userId === null && data.teamId && data.teamId === null) { badRequest(res, 'A website must be assigned to a User or Team.'); } diff --git a/tsconfig.json b/tsconfig.json index cbb5413f..b022d603 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,6 @@ "noEmit": true, "jsx": "preserve" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "queries/admin/website/getAllWebsites.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } From d60ad1c78206d4d965b185f53b13d7a1e7af35d3 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 21 Nov 2022 16:44:42 -0800 Subject: [PATCH 17/38] Fix isAdmin check. Fix Schema. --- db/postgresql/schema.prisma | 10 ++++------ pages/api/teams/[id]/user.ts | 12 +++++++++--- pages/api/users/[id]/role.ts | 8 -------- pages/api/websites/index.ts | 6 +++++- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index d47b9d55..962d9a15 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -18,8 +18,7 @@ model User { groupUser GroupUser[] userRole UserRole[] teamUser TeamUser[] - Website Website? @relation(fields: [websiteId], references: [id]) - websiteId String? @db.Uuid + Website Website[] @@map("user") } @@ -52,8 +51,8 @@ model Website { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - team Team[] - user User[] + team Team? @relation(fields: [teamId], references: [id]) + user User? @relation(fields: [userId], references: [id]) @@index([createdAt]) @@index([shareId]) @@ -183,8 +182,7 @@ model Team { teamUsers TeamUser[] UserRole UserRole[] - Website Website? @relation(fields: [websiteId], references: [id]) - websiteId String? @db.Uuid + Website Website[] @@map("team") } diff --git a/pages/api/teams/[id]/user.ts b/pages/api/teams/[id]/user.ts index 529f0195..8a0bb98b 100644 --- a/pages/api/teams/[id]/user.ts +++ b/pages/api/teams/[id]/user.ts @@ -4,8 +4,8 @@ import { UmamiApi } from 'lib/constants'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, deleteTeamUser, getUsersByTeamId } from 'queries'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; +import { createTeamUser, deleteTeamUser, getUsersByTeamId, getTeamUser } from 'queries'; export interface TeamUserRequestQuery { id: string; @@ -41,6 +41,13 @@ export default async ( const { user_id: userId } = req.body; + // Check for TeamUser + const teamUser = getTeamUser({ userId, teamId }); + + if (!teamUser) { + return badRequest(res, 'The User already exists on this Team.'); + } + const updated = await createTeamUser({ id: uuid(), userId, teamId }); return ok(res, updated); @@ -50,7 +57,6 @@ export default async ( if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { return unauthorized(res, 'You must be the owner of this team.'); } - const { team_user_id } = req.body; await deleteTeamUser(team_user_id); diff --git a/pages/api/users/[id]/role.ts b/pages/api/users/[id]/role.ts index aab90ebd..fe3dcbd4 100644 --- a/pages/api/users/[id]/role.ts +++ b/pages/api/users/[id]/role.ts @@ -42,7 +42,6 @@ export default async ( if (req.method === 'POST') { const { roleId, teamId } = req.body; - // Check when userRolename changes const userRole = getUserRole({ userId: id, roleId, teamId }); if (userRole) { @@ -57,13 +56,6 @@ export default async ( if (req.method === 'DELETE') { const { userRoleId } = req.body; - // Check when userRolename changes - const userRole = getUserRole({ id: userRoleId }); - - if (userRole) { - return badRequest(res, 'Role already exists for User.'); - } - const updated = await deleteUserRole(userRoleId); return ok(res, updated); diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index 50469efb..85da6c10 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -5,6 +5,8 @@ import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok } from 'next-basics'; import { createWebsite, getAllWebsites, getWebsitesByUserId } from 'queries'; +import { checkPermission } from 'lib/auth'; +import { UmamiApi } from 'lib/constants'; export interface WebsitesRequestQuery { include_all?: boolean; @@ -25,12 +27,14 @@ export default async ( await useAuth(req, res); const { - user: { id: userId, isAdmin }, + user: { id: userId }, } = req.auth; if (req.method === 'GET') { const { include_all } = req.query; + const isAdmin = await checkPermission(req, UmamiApi.Permission.Admin); + const websites = isAdmin && include_all ? await getAllWebsites() : await getWebsitesByUserId(userId); From 2259ee8d760687e930fa311dc03aae447c9d9566 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 21 Nov 2022 22:32:55 -0800 Subject: [PATCH 18/38] Initial conversion to react-basics. --- components/forms/Form.module.css | 63 ++ components/forms/LoginForm.js | 149 ++--- lib/client.ts | 14 + lib/middleware.js | 2 +- lib/redis.js | 30 +- package.json | 8 +- pages/_app.js | 42 +- styles/index.css | 51 +- tsconfig.json | 2 +- yarn.lock | 972 ++++++++++++++++++------------- 10 files changed, 724 insertions(+), 609 deletions(-) create mode 100644 components/forms/Form.module.css create mode 100644 lib/client.ts diff --git a/components/forms/Form.module.css b/components/forms/Form.module.css new file mode 100644 index 00000000..9dccd9b3 --- /dev/null +++ b/components/forms/Form.module.css @@ -0,0 +1,63 @@ +.form { + display: flex; + flex-direction: column; + gap: 30px; + width: 300px; + margin: 0 auto; +} + +.header { + font-size: 24px; + font-weight: 700; + text-align: center; + margin: 30px auto; +} + +.info { + text-align: center; + padding: 30px 0; +} + +.footer { + display: flex; + flex-direction: column; + gap: 20px; + font-size: 14px; + text-align: center; + margin: 30px auto; +} + +.footer a { + font-weight: 600; +} + +.buttons { + justify-content: center; +} + +.button { + flex: 1; + justify-content: center; +} + +.error { + width: 600px; + margin: 0 auto 30px; + background: var(--gray50); + padding: 16px; + color: var(--red400); + border: 1px solid var(--red400); + border-radius: 5px; + text-align: center; +} + +.success { + width: 600px; + margin: 60px auto; + background: var(--gray50); + padding: 16px; + color: var(--green400); + border: 1px solid var(--green400); + border-radius: 5px; + text-align: center; +} diff --git a/components/forms/LoginForm.js b/components/forms/LoginForm.js index 6a6560f1..894bc9b9 100644 --- a/components/forms/LoginForm.js +++ b/components/forms/LoginForm.js @@ -1,113 +1,62 @@ -import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Formik, Form, Field } from 'formik'; -import { setItem } from 'next-basics'; -import { useRouter } from 'next/router'; -import Button from 'components/common/Button'; -import FormLayout, { +import { useRef } from 'react'; +import { useMutation } from '@tanstack/react-query'; +import { + Form, + FormInput, FormButtons, - FormError, - FormMessage, - FormRow, -} from 'components/layout/FormLayout'; -import Icon from 'components/common/Icon'; -import useApi from 'hooks/useApi'; -import { AUTH_TOKEN } from 'lib/constants'; + TextField, + PasswordField, + SubmitButton, + Icon, +} from 'react-basics'; +import { useRouter } from 'next/router'; +import { useApi } from 'next-basics'; import { setUser } from 'store/app'; +import { setAuthToken } from 'lib/client'; import Logo from 'assets/logo.svg'; -import styles from './LoginForm.module.css'; - -const validate = ({ username, password }) => { - const errors = {}; - - if (!username) { - errors.username = ; - } - if (!password) { - errors.password = ; - } - - return errors; -}; +import styles from './Form.module.css'; export default function LoginForm() { - const { post } = useApi(); const router = useRouter(); - const [message, setMessage] = useState(); + const { post } = useApi(); + const { mutate, error, isLoading } = useMutation(data => post('/auth/login', data)); + const ref = useRef(); - const handleSubmit = async ({ username, password }) => { - const { ok, status, data } = await post('/auth/login', { - username, - password, + const handleSubmit = async data => { + mutate(data, { + onSuccess: async ({ token, account }) => { + setAuthToken(token); + setUser(account); + + await router.push('/websites'); + }, + onError: async () => { + ref.current.reset(undefined, { keepDirty: true, keepValues: true }); + }, }); - - if (ok) { - const { user, token } = data; - - setItem(AUTH_TOKEN, token); - - setUser(user); - - await router.push('/'); - - return null; - } else { - setMessage( - status === 401 ? ( - - ) : ( - data - ), - ); - } }; return ( - - - {() => ( -
-
- } size="xlarge" className={styles.icon} /> -

umami

-
- - -
- - -
-
- - -
- - -
-
- - - - {message} -
- )} -
-
+ <> +
+ + + +

umami

+
+
+ + + + + + + + + Log in + + +
+ ); } diff --git a/lib/client.ts b/lib/client.ts new file mode 100644 index 00000000..df68c6f8 --- /dev/null +++ b/lib/client.ts @@ -0,0 +1,14 @@ +import { getItem, setItem, removeItem } from 'next-basics'; +import { AUTH_TOKEN } from './constants'; + +export function getAuthToken() { + return getItem(AUTH_TOKEN); +} + +export function setAuthToken(token) { + setItem(AUTH_TOKEN, token); +} + +export function removeAuthToken() { + removeItem(AUTH_TOKEN); +} diff --git a/lib/middleware.js b/lib/middleware.js index c60d2770..5bdad75d 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -3,7 +3,7 @@ import debug from 'debug'; import cors from 'cors'; import { validate } from 'uuid'; import { findSession } from 'lib/session'; -import { parseShareToken, getAuthToken } from 'lib/auth'; +import { getAuthToken, parseShareToken } from 'lib/auth'; import { secret } from 'lib/crypto'; import redis from 'lib/redis'; import { getUser } from '../queries'; diff --git a/lib/redis.js b/lib/redis.js index b2ea2279..5ec4147d 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -1,39 +1,39 @@ +import { createClient } from 'redis'; import debug from 'debug'; -import Redis from 'ioredis'; -import { REDIS } from 'lib/db'; const log = debug('umami:redis'); -export const DELETED = 'deleted'; +const REDIS = Symbol(); let redis; const enabled = Boolean(process.env.REDIS_URL); -function getClient() { - if (!enabled) { +async function getClient() { + if (!process.env.REDIS_URL) { return null; } - const redis = new Redis(process.env.REDIS_URL, { - retryStrategy(times) { - log(`Redis reconnecting attempt: ${times}`); - return 5000; - }, - }); + const client = createClient({ url: process.env.REDIS_URL }); + client.on('error', err => log(err)); + await client.connect(); if (process.env.NODE_ENV !== 'production') { - global[REDIS] = redis; + global[REDIS] = client; } log('Redis initialized'); - return redis; + return client; } async function get(key) { await connect(); + const data = await redis.get(key); + + log({ key, data }); + try { - return JSON.parse(await redis.get(key)); + return JSON.parse(data); } catch { return null; } @@ -53,7 +53,7 @@ async function del(key) { async function connect() { if (!redis && enabled) { - redis = global[REDIS] || getClient(); + redis = global[REDIS] || (await getClient()); } return redis; diff --git a/package.json b/package.json index 320283c4..5bc57774 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "dependencies": { "@fontsource/inter": "4.5.7", "@prisma/client": "4.5.0", + "@tanstack/react-query": "^4.16.1", "chalk": "^4.1.1", "chart.js": "^2.9.4", "classnames": "^2.3.1", @@ -74,7 +75,6 @@ "formik": "^2.2.9", "fs-extra": "^10.0.1", "immer": "^9.0.12", - "ioredis": "^5.2.3", "ipaddr.js": "^2.0.1", "is-ci": "^3.0.1", "is-docker": "^3.0.0", @@ -88,15 +88,17 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "prop-types": "^15.7.2", - "react": "^17.0.0", + "react": "^18.2.0", + "react-basics": "^0.29.0", "react-beautiful-dnd": "^13.1.0", - "react-dom": "^17.0.0", + "react-dom": "^18.2.0", "react-intl": "^5.24.7", "react-simple-maps": "^2.3.0", "react-spring": "^9.4.4", "react-tooltip": "^4.2.21", "react-use-measure": "^2.0.4", "react-window": "^1.8.6", + "redis": "^4.5.0", "request-ip": "^3.3.0", "semver": "^7.3.6", "thenby": "^1.3.4", diff --git a/pages/_app.js b/pages/_app.js index c3617a08..fb4e8e44 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,14 +1,16 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; import { IntlProvider } from 'react-intl'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import useLocale from 'hooks/useLocale'; import useConfig from 'hooks/useConfig'; -import 'styles/variables.css'; -import 'styles/bootstrap-grid.css'; +import 'react-basics/dist/styles.css'; import 'styles/index.css'; import '@fontsource/inter/400.css'; import '@fontsource/inter/600.css'; +const client = new QueryClient(); + export default function App({ Component, pageProps }) { const { locale, messages } = useLocale(); const { basePath } = useRouter(); @@ -22,22 +24,24 @@ export default function App({ Component, pageProps }) { } return ( - - - - - - - - - - - - - -
- -
-
+ + + + + + + + + + + + + + +
+ +
+
+
); } diff --git a/styles/index.css b/styles/index.css index b6006b43..41c66f16 100644 --- a/styles/index.css +++ b/styles/index.css @@ -3,6 +3,7 @@ body { font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantrell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + font-size: 16px; font-weight: 400; line-height: 1.8; padding: 0; @@ -12,9 +13,6 @@ body { display: flex; flex-direction: column; flex: 1; - - font-size: var(--font-size-normal); - overflow-y: overlay; } body { @@ -70,38 +68,12 @@ h6 { margin: 0; } -button, -input, -select { - font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; -} - a, a:active, a:visited { color: var(--primary400); } -input[type='text'], -input[type='password'], -select, -textarea { - color: var(--gray900); - background: var(--gray50); - padding: 4px 8px; - font-size: var(--font-size-normal); - line-height: 1.8; - border: 1px solid var(--gray500); - border-radius: 4px; - outline: none; - resize: none; - flex: 1; -} - -input[type='checkbox'] + label { - margin-left: 10px; -} - label { flex: 1; margin-right: 20px; @@ -141,24 +113,3 @@ svg { #__modals { z-index: 10; } - -.container { - padding: 0; - display: flex; - flex-direction: column; - flex: 1; -} - -.row { - margin-right: 0; - margin-left: 0; -} -.row > .col, -.row > [class*='col-'] { - padding-right: 0; - padding-left: 0; -} - -.center { - text-align: center; -} diff --git a/tsconfig.json b/tsconfig.json index cbb5413f..b022d603 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,6 @@ "noEmit": true, "jsx": "preserve" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "queries/admin/website/getAllWebsites.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index f0229aa3..d3f9f408 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1050,21 +1050,21 @@ "@babel/plugin-transform-typescript" "^7.18.6" "@babel/runtime-corejs3@^7.10.2": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.20.0.tgz#56ef7af3cd23d1570969809a5a8782e774e0141a" - integrity sha512-v1JH7PeAAGBEyTQM9TqojVl+b20zXtesFKCJHu50xMxZKD1fX0TKaKHPsZfFkXfs7D1M9M6Eeqg1FkJ3a0x2dA== + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.20.1.tgz#d0775a49bb5fba77e42cbb7276c9955c7b05af8d" + integrity sha512-CGulbEDcg/ND1Im7fUNRZdGXmX2MTWVVZacQi/6DiKE5HNwZ3aVTm5PV4lO8HHz0B2h8WQyvKKjbX5XgTtydsg== dependencies: core-js-pure "^3.25.1" regenerator-runtime "^0.13.10" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz" - integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.9", "@babel/runtime@^7.9.2": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" + integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== dependencies: - regenerator-runtime "^0.13.4" + regenerator-runtime "^0.13.10" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.18.9", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.8.4": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.0.tgz#824a9ef325ffde6f78056059db3168c08785e24a" integrity sha512-NDYdls71fTXoU8TZHfbBWg7DiZfNzClcKui/+kyi6ppD2L1qnWW3VV6CjtaBXSUGGhiTWJ6ereOIkUvenif66Q== @@ -1579,9 +1579,9 @@ unstorage "^0.6.0" "@netlify/plugin-nextjs@^4.27.3": - version "4.28.3" - resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-4.28.3.tgz#e1d3d413a75605efcedc2f3fc27501f9bc8a49e6" - integrity sha512-fA+gOkIowfQVaL1YDF3t+3uCsZ7OEkandLpFIv0D1faA/6Fy7VxCWhAxowRQ6oV3SjYxsV259aRMT0LaRejLMw== + version "4.29.2" + resolved "https://registry.yarnpkg.com/@netlify/plugin-nextjs/-/plugin-nextjs-4.29.2.tgz#b59e484d12ccc50c54282dd49fcfe27cc692e7d0" + integrity sha512-Sxaf+eITuDEMUOQ5ySMMDibwi3KmLgbQ5gLVDFuOnm/YDe9l3cHTwJf0Sn4BkvDPTaz6p0kkxAHo2ANt91qwuA== dependencies: "@netlify/esbuild" "0.14.39" "@netlify/functions" "^1.3.0" @@ -1606,82 +1606,82 @@ slash "^3.0.0" tiny-glob "^0.2.9" -"@next/env@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.2.tgz#fb819366771f5721e9438ca3a42ad18684f0949b" - integrity sha512-upwtMaHxlv/udAWGq0kE+rg8huwmcxQPsKZFhS1R5iVO323mvxEBe1YrSXe1awLbg9sTIuEHbgxjLLt7JbeuAQ== +"@next/env@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.3.tgz#26c8e8f0f65da382d1a537cb8df30d63fc5d828a" + integrity sha512-H2pKuOasV9RgvVaWosB2rGSNeQShQpiDaF4EEjLyagIc3HwqdOw2/VAG/8Lq+adOwPv2P73O1hulTNad3k5MDw== -"@next/eslint-plugin-next@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.2.tgz#c965bb41fd5c36b6fc5fe066aeebf9385b69a62d" - integrity sha512-Jx0BIS9STamGSbA+vgTxPAi1mRcq4DoH/5kPMs0PFLFi542mmIKUUtYWB61bI+G6KPMTqejYmO3zi+SF6tzGYg== +"@next/eslint-plugin-next@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.3.tgz#4c2eb595101a1778d5ff7c78574f7e810f72b5e5" + integrity sha512-s1mPMhhmwc+B97lQ2xzLLEdn3TR6ietc8Z1zLhAEd5Vujqx+Ks7E8Qr8V93I/qTs21WY66zvs1SXKYLvOHbQVw== dependencies: glob "7.1.7" -"@next/swc-android-arm-eabi@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.2.tgz#806e3be9741bc14aafdfad0f0c4c6a8de5b77ee1" - integrity sha512-r2rrz+DZ8YYGqzVrbRrpP6GKzwozpOrnFbErc4k36vUTSFMag9yQahZfaBe06JYdqu/e5yhm/saIDEaSVPRP4g== +"@next/swc-android-arm-eabi@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.3.tgz#1173a8e9ddb92c9d2d1a4fc29c5397f3d815c1ef" + integrity sha512-5O/ZIX6hlIRGMy1R2f/8WiCZ4Hp4WTC0FcTuz8ycQ28j/mzDnmzjVoayVVr+ZmfEKQayFrRu+vxHjFyY0JGQlQ== -"@next/swc-android-arm64@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.2.tgz#f9ec6b7fc746832a217ad6bb5478624d1a9a9822" - integrity sha512-B+TINJhCf+CrY1+b3/JWQlkecv53rAGa/gA7gi5B1cnBa/2Uvoe+Ue0JeCefTjfiyl1ScsyNx+NcESY8Ye2Ngg== +"@next/swc-android-arm64@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.3.tgz#8e49a1486ff1c5e6f4760ad31a2fa3cfcc5a3329" + integrity sha512-2QWreRmlxYRDtnLYn+BI8oukHwcP7W0zGIY5R2mEXRjI4ARqCLdu8RmcT9Vemw7RfeAVKA/4cv/9PY0pCcQpNA== -"@next/swc-darwin-arm64@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.2.tgz#97c532d35c66ce6b6941ae24b5b8b267b9b0d0d8" - integrity sha512-PTUfe1ZrwjsiuTmr3bOM9lsoy5DCmfYsLOUF9ZVhtbi5MNJVmUTy4VZ06GfrvnCO5hGCr48z3vpFE9QZ0qLcPw== +"@next/swc-darwin-arm64@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.3.tgz#92618ffea1d0d128db0787854afe368b5976837a" + integrity sha512-GtZdDLerM+VToCMFp+W+WhnT6sxHePQH4xZZiYD/Y8KFiwHbDRcJr2FPG0bAJnGNiSvv/QQnBq74wjZ9+7vhcQ== -"@next/swc-darwin-x64@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.2.tgz#e0cb4ff4b11faaff3a891bd1d18ed72f71e30ebe" - integrity sha512-1HkjmS9awwlaeEY8Y01nRSNkSv3y+qnC/mjMPe/W66hEh3QKa/LQHqHeS7NOdEs19B2mhZ7w+EgMRXdLQ0Su8w== +"@next/swc-darwin-x64@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.3.tgz#76a5a496cc7ead3cc02aaca84d1ed02dff86e029" + integrity sha512-gRYvTKrRYynjFQUDJ+upHMcBiNz0ii0m7zGgmUTlTSmrBWqVSzx79EHYT7Nn4GWHM+a/W+2VXfu+lqHcJeQ9gQ== -"@next/swc-freebsd-x64@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.2.tgz#d7b93dd344cb67d1969565d0796c7b7d0217fccf" - integrity sha512-h5Mx0BKDCJ5Vu/U8e07esF6PjPv1EJgmRbYWTUZMAflu13MQpCJkKEJir7+BeRfTXRfgFf+llc7uocrpd7mcrg== +"@next/swc-freebsd-x64@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.3.tgz#d546fb7060adf0cd27c6a8c1abca5c58f62c8f06" + integrity sha512-r+GLATzCjjQI82bgrIPXWEYBwZonSO64OThk5wU6HduZlDYTEDxZsFNoNoesCDWCgRrgg+OXj7WLNy1WlvfX7w== -"@next/swc-linux-arm-gnueabihf@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.2.tgz#c2170a89effe00fdd65798c99684fd93a02b197c" - integrity sha512-EuRZAamoxfe/WoWRaC0zsCAoE4gs/mEhilcloNM4J5Mnb3PLY8PZV394W7t5tjBjItMCF7l2Ebwjwtm46tq2RA== +"@next/swc-linux-arm-gnueabihf@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.3.tgz#525f451e6e1d134e064707c5c761b6d5d6bb3c7e" + integrity sha512-juvRj1QX9jmQScL4nV0rROtYUFgWP76zfdn1fdfZ2BhvwUugIAq8x+jLVGlnXKUhDrP9+RrAufqXjjVkK+uBxA== -"@next/swc-linux-arm64-gnu@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.2.tgz#26df7d7cdc18cf413f12a408179ee4ac315f383a" - integrity sha512-T9GCFyOIb4S3acA9LqflUYD+QZ94iZketHCqKdoO0Nx0OCHIgGJV5rotDe8TDXwh/goYpIfyHU4j1qqw4w4VnA== +"@next/swc-linux-arm64-gnu@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.3.tgz#13aa5dfeef0de52eac1220ab22cabdee6447bb3a" + integrity sha512-hzinybStPB+SzS68hR5rzOngOH7Yd/jFuWGeg9qS5WifYXHpqwGH2BQeKpjVV0iJuyO9r309JKrRWMrbfhnuBA== -"@next/swc-linux-arm64-musl@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.2.tgz#fd42232a6b10d9f9a4f71433d59c280a4532d06f" - integrity sha512-hxNVZS6L3c2z3l9EH2GP0MGQ9exu6O8cohYNZyqC9WUl6C03sEn8xzDH1y+NgD3fVurvYkGU5F0PDddJJLfDIw== +"@next/swc-linux-arm64-musl@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.3.tgz#4bdae82882c1e31a1008f20f544b1954a21d385f" + integrity sha512-oyfQYljCwf+9zUu1YkTZbRbyxmcHzvJPMGOxC3kJOReh3kCUoGcmvAxUPMtFD6FSYjJ+eaog4+2IFHtYuAw/bQ== -"@next/swc-linux-x64-gnu@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.2.tgz#5307579e3d8fbdb03adbe6cfc915b51548e0a103" - integrity sha512-fCPkLuwDwY8/QeXxciJJjDHG09liZym/Bhb4A+RLFQ877wUkwFsNWDUTSdUx0YXlYK/1gf67BKauqKkOKp6CYw== +"@next/swc-linux-x64-gnu@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.3.tgz#0697b1fc60dc4a86a7260f60e983d9064a331b2c" + integrity sha512-epv4FMazj/XG70KTTnrZ0H1VtL6DeWOvyHLHYy7f5PdgDpBXpDTFjVqhP8NFNH8HmaDDdeL1NvQD07AXhyvUKA== -"@next/swc-linux-x64-musl@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.2.tgz#d5cb920a825a8dc80ffba8a6b797fb845af0b84c" - integrity sha512-o+GifBIQ2K+/MEFxHsxUZoU3bsuVFLXZYWd3idimFHiVdDCVYiKsY6mYMmKDlucX+9xRyOCkKL9Tjf+3tuXJpw== +"@next/swc-linux-x64-musl@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.3.tgz#af012e65035fcd7cc3855ce90b4095d2c7b879a5" + integrity sha512-bG5QODFy59XnSFTiPyIAt+rbPdphtvQMibtOVvyjwIwsBUw7swJ6k+6PSPVYEYpi6SHzi3qYBsro39ayGJKQJg== -"@next/swc-win32-arm64-msvc@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.2.tgz#2a0d619e5bc0cec17ed093afd1ca6b1c37c2690c" - integrity sha512-crii66irzGGMSUR0L8r9+A06eTv7FTXqw4rgzJ33M79EwQJOdpY7RVKXLQMurUhniEeQEEOfamiEdPIi/qxisw== +"@next/swc-win32-arm64-msvc@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.3.tgz#bf717c96ce17f63840e3cbb023725cb3872ed0b3" + integrity sha512-FbnT3reJ3MbTJ5W0hvlCCGGVDSpburzT5XGC9ljBJ4kr+85iNTLjv7+vrPeDdwHEqtGmdZgnabkLVCI4yFyCag== -"@next/swc-win32-ia32-msvc@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.2.tgz#769bef60d0d678c3d7606a4dc7fee018d6199227" - integrity sha512-5hRUSvn3MdQ4nVRu1rmKxq5YJzpTtZfaC/NyGw6wa4NSF1noUn/pdQGUr+I5Qz3CZkd1gZzzC0eaXQHlrk0E2g== +"@next/swc-win32-ia32-msvc@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.3.tgz#f434bc4bd952af77070868b5fa32ced85b52a646" + integrity sha512-M/fKZC2tMGWA6eTsIniNEBpx2prdR8lIxvSO3gv5P6ymZOGVWCvEMksnTkPAjHnU6d8r8eCiuGKm3UNo7zCTpQ== -"@next/swc-win32-x64-msvc@12.3.2": - version "12.3.2" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.2.tgz#45beb4b9d28e6dd6abf63cab1c5b92dc84323a6b" - integrity sha512-tpQJYUH+TzPMIsdVl9fH8uDg47iwiNjKY+8e9da3dXqlkztKzjSw0OwSADoqh3KrifplXeKSta+BBGLdBqg3sg== +"@next/swc-win32-x64-msvc@12.3.3": + version "12.3.3" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.3.tgz#1b412e9e15550680e1fdba70daa8b6ddcc75a035" + integrity sha512-Ku9mfGwmNtk44o4B/jEWUxBAT4tJ3S7QbBMLJdL1GmtRZ05LGL36OqWjLvBPr8dFiHOQQbYoAmYfQw7zeGypYA== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1721,91 +1721,125 @@ resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.5.0.tgz#82df347a893a5ae2a67707d44772ba181f4b9328" integrity sha512-4t9ir2SbQQr/wMCNU4YpHWp5hU14J2m3wHUZnGJPpmBF8YtkisxyVyQsKd1e6FyLTaGq8LOLhm6VLYHKqKNm+g== -"@react-spring/animated@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/animated/-/animated-9.5.2.tgz" - integrity sha512-oRlX+MmYLbK8IuUZR7SQUnRjXxJ4PMIZeBkBd1SUWVgVJAHMTfJzPltzm+I6p59qX+qLlklYHfnWaonQKDqLuQ== +"@react-spring/animated@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.5.5.tgz#d3bfd0f62ed13a337463a55d2c93bb23c15bbf3e" + integrity sha512-glzViz7syQ3CE6BQOwAyr75cgh0qsihm5lkaf24I0DfU63cMm/3+br299UEYkuaHNmfDfM414uktiPlZCNJbQA== dependencies: - "@react-spring/shared" "~9.5.2" - "@react-spring/types" "~9.5.2" + "@react-spring/shared" "~9.5.5" + "@react-spring/types" "~9.5.5" -"@react-spring/core@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/core/-/core-9.5.2.tgz" - integrity sha512-UMRtFH6EfebMp/NMDGCUY5+hZFXsg9iT9hzt/iPzJSz2WMXKBjLoFZHJXcmiVOrIhzHmg1O0pFECn1Wp6pZ5Gw== +"@react-spring/core@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.5.5.tgz#1d8a4c64630ee26b2295361e1eedfd716a85b4ae" + integrity sha512-shaJYb3iX18Au6gkk8ahaF0qx0LpS0Yd+ajb4asBaAQf6WPGuEdJsbsNSgei1/O13JyEATsJl20lkjeslJPMYA== dependencies: - "@react-spring/animated" "~9.5.2" - "@react-spring/rafz" "~9.5.2" - "@react-spring/shared" "~9.5.2" - "@react-spring/types" "~9.5.2" + "@react-spring/animated" "~9.5.5" + "@react-spring/rafz" "~9.5.5" + "@react-spring/shared" "~9.5.5" + "@react-spring/types" "~9.5.5" -"@react-spring/konva@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/konva/-/konva-9.5.2.tgz" - integrity sha512-FN8LpbGQtm2pllU9mOyYjYwvLtA9EiIPWk2NVuhhX+5lJZrdCWuEY7EyFpK8PtgZXBdVj8bj7eIu1LlTnARW/A== +"@react-spring/konva@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/konva/-/konva-9.5.5.tgz#ddbb30cfa268219d69552aa71188832ca8ab4905" + integrity sha512-0CNh+1vCIjNUklTFwMvxg+H83Jo2OWykBrdEA28ccmnpZgkQ8Kq5xyvaPFLzcDKV67OXHnaWiCYKpRbhLy2wng== dependencies: - "@react-spring/animated" "~9.5.2" - "@react-spring/core" "~9.5.2" - "@react-spring/shared" "~9.5.2" - "@react-spring/types" "~9.5.2" + "@react-spring/animated" "~9.5.5" + "@react-spring/core" "~9.5.5" + "@react-spring/shared" "~9.5.5" + "@react-spring/types" "~9.5.5" -"@react-spring/native@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/native/-/native-9.5.2.tgz" - integrity sha512-G9BCAKVADLweLR43uyMnTrOnYDb4BboYvqKY+0X1fLs45PNrfbBXnSLot4g+5x3HjblypJgNq7CjHlqZKI980g== +"@react-spring/native@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/native/-/native-9.5.5.tgz#4ecc420c7b4c3fefeebd55d852640d36c29ec9c8" + integrity sha512-kauqmyJ8u7aVy2bBs22vl1SdB2i5uYIL4rP53k1KDWrFSqJh4j3efWkbTt9uzR5cMXuNVbkNo9OYVFUcQBz50A== dependencies: - "@react-spring/animated" "~9.5.2" - "@react-spring/core" "~9.5.2" - "@react-spring/shared" "~9.5.2" - "@react-spring/types" "~9.5.2" + "@react-spring/animated" "~9.5.5" + "@react-spring/core" "~9.5.5" + "@react-spring/shared" "~9.5.5" + "@react-spring/types" "~9.5.5" -"@react-spring/rafz@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.5.2.tgz" - integrity sha512-xHSRXKKBI/wDUkZGrspkOm4VlgN6lZi8Tw9Jzibp9QKf3neoof+U2mDNgklvnLaasymtUwAq9o4ZfFvQIVNgPQ== +"@react-spring/rafz@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.5.5.tgz#62a49c5e294104b79db2a8afdf4f3a274c7f44ca" + integrity sha512-F/CLwB0d10jL6My5vgzRQxCNY2RNyDJZedRBK7FsngdCmzoq3V4OqqNc/9voJb9qRC2wd55oGXUeXv2eIaFmsw== -"@react-spring/shared@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/shared/-/shared-9.5.2.tgz" - integrity sha512-/OSf2sjwY4BUnjZL6xMC+H3WxOOhMUCk+yZwgdj40XuyUpk6E6tYyiPeD9Yq5GLsZHodkvE1syVMRVReL4ndAg== +"@react-spring/shared@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.5.5.tgz#9be0b391d546e3e184a24ecbaf40acbaeab7fc73" + integrity sha512-YwW70Pa/YXPOwTutExHZmMQSHcNC90kJOnNR4G4mCDNV99hE98jWkIPDOsgqbYx3amIglcFPiYKMaQuGdr8dyQ== dependencies: - "@react-spring/rafz" "~9.5.2" - "@react-spring/types" "~9.5.2" + "@react-spring/rafz" "~9.5.5" + "@react-spring/types" "~9.5.5" -"@react-spring/three@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/three/-/three-9.5.2.tgz" - integrity sha512-3H7Lv8BJZ3dajh0yJA3m9rEbqz5ZNrTCAkhVOeLqgvBlcWU5qVs4luYA1Z7H4vZnLqVtzv+kHAyg3XIpuTOXhQ== +"@react-spring/three@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/three/-/three-9.5.5.tgz#c6fbee977007d1980406db20a28ac3f5dc2ce153" + integrity sha512-9kTIaSceqFIl5EIrdwM7Z53o5I+9BGNVzbp4oZZYMao+GMAWOosnlQdDG5GeqNsIqfW9fZCEquGqagfKAxftcA== dependencies: - "@react-spring/animated" "~9.5.2" - "@react-spring/core" "~9.5.2" - "@react-spring/shared" "~9.5.2" - "@react-spring/types" "~9.5.2" + "@react-spring/animated" "~9.5.5" + "@react-spring/core" "~9.5.5" + "@react-spring/shared" "~9.5.5" + "@react-spring/types" "~9.5.5" -"@react-spring/types@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/types/-/types-9.5.2.tgz" - integrity sha512-n/wBRSHPqTmEd4BFWY6TeR1o/UY+3ujoqMxLjqy90CcY/ozJzDRuREL3c+pxMeTF2+B7dX33dTPCtFMX51nbxg== +"@react-spring/types@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.5.5.tgz#c8e94f1b9232ca7cb9d860ea67762ec401b1de14" + integrity sha512-7I/qY8H7Enwasxr4jU6WmtNK+RZ4Z/XvSlDvjXFVe7ii1x0MoSlkw6pD7xuac8qrHQRm9BTcbZNyeeKApYsvCg== -"@react-spring/web@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/web/-/web-9.5.2.tgz" - integrity sha512-cusTjbOGTgtbsnpBDjb6Ia+B0lQLE0Fk5rGDog6Sww7hWnLIQ521PMiOBnAWtkntB9eXDUfj7L91nwJviEC0lw== +"@react-spring/web@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.5.5.tgz#d416abc591aaed930401f0c98a991a8c5b90c382" + integrity sha512-+moT8aDX/ho/XAhU+HRY9m0LVV9y9CK6NjSRaI+30Re150pB3iEip6QfnF4qnhSCQ5drpMF0XRXHgOTY/xbtFw== dependencies: - "@react-spring/animated" "~9.5.2" - "@react-spring/core" "~9.5.2" - "@react-spring/shared" "~9.5.2" - "@react-spring/types" "~9.5.2" + "@react-spring/animated" "~9.5.5" + "@react-spring/core" "~9.5.5" + "@react-spring/shared" "~9.5.5" + "@react-spring/types" "~9.5.5" -"@react-spring/zdog@~9.5.2": - version "9.5.2" - resolved "https://registry.npmjs.org/@react-spring/zdog/-/zdog-9.5.2.tgz" - integrity sha512-zUX8RzX8gM51g8NJ5Qaf15KNKQgN3qN/8m5FvqmiqZ5ZGqjoHkbCoMD3o2MICTUN1l+d4eUu9TYrmiO2bgJo/g== +"@react-spring/zdog@~9.5.5": + version "9.5.5" + resolved "https://registry.yarnpkg.com/@react-spring/zdog/-/zdog-9.5.5.tgz#916dba337637d1151c3c2bc829b5105d15adacb5" + integrity sha512-LZgjo2kLlGmUqfE2fdVnvLXz+4eYyQARRvB9KQ4PTEynaETTG89Xgn9YxLrh1p57DzH7gEmTGDZ5hEw3pWqu8g== dependencies: - "@react-spring/animated" "~9.5.2" - "@react-spring/core" "~9.5.2" - "@react-spring/shared" "~9.5.2" - "@react-spring/types" "~9.5.2" + "@react-spring/animated" "~9.5.5" + "@react-spring/core" "~9.5.5" + "@react-spring/shared" "~9.5.5" + "@react-spring/types" "~9.5.5" + +"@redis/bloom@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.1.0.tgz#64e310ddee72010676e14296076329e594a1f6c7" + integrity sha512-9QovlxmpRtvxVbN0UBcv8WfdSMudNZZTFqCsnBszcQXqaZb/TVe30ScgGEO7u1EAIacTPAo7/oCYjYAxiHLanQ== + +"@redis/client@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.4.0.tgz#d2c56ce26c3e2fe3412db5cfb1814169662167eb" + integrity sha512-1gEj1AkyXPlkcC/9/T5xpDcQF8ntERURjLBgEWMTdUZqe181zfI9BY3jc2OzjTLkvZh5GV7VT4ktoJG2fV2ufw== + dependencies: + cluster-key-slot "1.1.1" + generic-pool "3.9.0" + yallist "4.0.0" + +"@redis/graph@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519" + integrity sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg== + +"@redis/json@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1" + integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw== + +"@redis/search@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.0.tgz#7abb18d431f27ceafe6bcb4dd83a3fa67e9ab4df" + integrity sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ== + +"@redis/time-series@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.4.tgz#af85eb080f6934580e4d3b58046026b6c2b18717" + integrity sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng== "@rollup/plugin-buble@^0.21.3": version "0.21.3" @@ -1951,6 +1985,19 @@ dependencies: tslib "^2.4.0" +"@tanstack/query-core@4.15.1": + version "4.15.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.15.1.tgz#a282f04fe5e612b50019e1cfaf0efabd220e00ce" + integrity sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ== + +"@tanstack/react-query@^4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.16.1.tgz#077006b8eb2c87fbe8d1597c1a0083a2d218b791" + integrity sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q== + dependencies: + "@tanstack/query-core" "4.15.1" + use-sync-external-store "^1.2.0" + "@trysound/sax@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" @@ -2015,7 +2062,7 @@ "@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" - resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== dependencies: "@types/react" "*" @@ -2063,12 +2110,12 @@ "@types/prop-types@*": version "15.7.5" - resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== "@types/react-redux@^7.1.20": version "7.1.24" - resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== dependencies: "@types/hoist-non-react-statics" "^3.3.0" @@ -2076,7 +2123,16 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react@*", "@types/react@16 || 17 || 18": +"@types/react@*": + version "18.0.25" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.25.tgz#8b1dcd7e56fe7315535a4af25435e0bb55c8ae44" + integrity sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/react@16 || 17 || 18": version "18.0.10" resolved "https://registry.npmjs.org/@types/react/-/react-18.0.10.tgz" integrity sha512-dIugadZuIPrRzvIEevIu7A1smqOAjkSMv8qOfwPt9Ve6i6JT/FQcCHyk2qIAxwsQNKZt5/oGR0T4z9h2dXRAkg== @@ -2087,7 +2143,7 @@ "@types/scheduler@*": version "0.16.2" - resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== "@types/schema-utils@^2.4.0": @@ -2098,47 +2154,47 @@ schema-utils "*" "@typescript-eslint/parser@^5.21.0": - version "5.42.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.42.0.tgz#be0ffbe279e1320e3d15e2ef0ad19262f59e9240" - integrity sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA== + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.44.0.tgz#99e2c710a2252191e7a79113264f438338b846ad" + integrity sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA== dependencies: - "@typescript-eslint/scope-manager" "5.42.0" - "@typescript-eslint/types" "5.42.0" - "@typescript-eslint/typescript-estree" "5.42.0" + "@typescript-eslint/scope-manager" "5.44.0" + "@typescript-eslint/types" "5.44.0" + "@typescript-eslint/typescript-estree" "5.44.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.42.0": - version "5.42.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz#e1f2bb26d3b2a508421ee2e3ceea5396b192f5ef" - integrity sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow== +"@typescript-eslint/scope-manager@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.44.0.tgz#988c3f34b45b3474eb9ff0674c18309dedfc3e04" + integrity sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g== dependencies: - "@typescript-eslint/types" "5.42.0" - "@typescript-eslint/visitor-keys" "5.42.0" + "@typescript-eslint/types" "5.44.0" + "@typescript-eslint/visitor-keys" "5.44.0" -"@typescript-eslint/types@5.42.0": - version "5.42.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.42.0.tgz#5aeff9b5eced48f27d5b8139339bf1ef805bad7a" - integrity sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw== +"@typescript-eslint/types@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.44.0.tgz#f3f0b89aaff78f097a2927fe5688c07e786a0241" + integrity sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ== -"@typescript-eslint/typescript-estree@5.42.0": - version "5.42.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz#2592d24bb5f89bf54a63384ff3494870f95b3fd8" - integrity sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg== +"@typescript-eslint/typescript-estree@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.44.0.tgz#0461b386203e8d383bb1268b1ed1da9bc905b045" + integrity sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw== dependencies: - "@typescript-eslint/types" "5.42.0" - "@typescript-eslint/visitor-keys" "5.42.0" + "@typescript-eslint/types" "5.44.0" + "@typescript-eslint/visitor-keys" "5.44.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@5.42.0": - version "5.42.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz#ee8d62d486f41cfe646632fab790fbf0c1db5bb0" - integrity sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg== +"@typescript-eslint/visitor-keys@5.44.0": + version "5.44.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.44.0.tgz#10740dc28902bb903d12ee3a005cc3a70207d433" + integrity sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ== dependencies: - "@typescript-eslint/types" "5.42.0" + "@typescript-eslint/types" "5.44.0" eslint-visitor-keys "^3.3.0" "@vercel/node-bridge@^2.1.0": @@ -2317,9 +2373,9 @@ ajv@^8.0.0, ajv@^8.8.0: uri-js "^4.2.2" ajv@^8.0.1: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" - integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + version "8.11.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.2.tgz#aecb20b50607acf2569b6382167b65a96008bb78" + integrity sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -2358,9 +2414,9 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: color-convert "^2.0.1" anymatch@^3.1.2, anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -2390,7 +2446,7 @@ array-find-index@^1.0.1: resolved "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz" integrity sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw== -array-includes@^3.1.4, array-includes@^3.1.5: +array-includes@^3.1.4: version "3.1.5" resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz" integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ== @@ -2401,6 +2457,17 @@ array-includes@^3.1.4, array-includes@^3.1.5: get-intrinsic "^1.1.1" is-string "^1.0.7" +array-includes@^3.1.5, array-includes@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" @@ -2416,16 +2483,27 @@ array.prototype.flat@^1.2.5: es-abstract "^1.19.2" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz#a7e8ed4225f4788a70cd910abcf0791e76a5534f" - integrity sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg== +array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +array.prototype.tosorted@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532" + integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.1.3" + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -2486,9 +2564,9 @@ aws4@^1.8.0: integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== axe-core@^4.4.3: - version "4.5.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.5.0.tgz#6efe2ecdba205fcc9d7ddb3d48c2cf630f70eb5e" - integrity sha512-4+rr8eQ7+XXS5nZrKcMO/AikHL0hVqy+lHWAnE3xdHl+aguag8SOQ6eEqLexwLNWgXIMfunGuD3ON1/6Kyet0A== + version "4.5.2" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.5.2.tgz#823fdf491ff717ac3c58a52631d4206930c1d9f7" + integrity sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA== axobject-query@^2.2.0: version "2.2.0" @@ -2686,11 +2764,16 @@ caniuse-lite@^1.0.30001335: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz" integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg== -caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001406: +caniuse-lite@^1.0.30001400: version "1.0.30001427" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001427.tgz#d3a749f74be7ae0671fbec3a4eea18576e8ad646" integrity sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ== +caniuse-lite@^1.0.30001406: + version "1.0.30001434" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz#ec1ec1cfb0a93a34a0600d37903853030520a4e5" + integrity sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" @@ -2758,7 +2841,7 @@ chownr@^1.1.1: chownr@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== ci-info@^3.2.0: @@ -2814,6 +2897,11 @@ clipboardy@^3.0.0: execa "^5.1.1" is-wsl "^2.2.0" +cluster-key-slot@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz#10ccb9ded0729464b6d2e7d714b100a2d1259d43" + integrity sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw== + cluster-key-slot@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz" @@ -2934,9 +3022,9 @@ core-js-compat@^3.25.1: browserslist "^4.21.4" core-js-pure@^3.25.1: - version "3.26.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.26.0.tgz#7ad8a5dd7d910756f3124374b50026e23265ca9a" - integrity sha512-LiN6fylpVBVwT8twhhluD9TzXmZQQsr2I2eIKtWNbZI1XMfBT7CV18itaN6RA7EtQd/SDdRx/wzvAShX2HvhQA== + version "3.26.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.26.1.tgz#653f4d7130c427820dcecd3168b594e8bb095a33" + integrity sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ== core-util-is@1.0.2: version "1.0.2" @@ -2951,10 +3039,10 @@ cors@^2.8.5: object-assign "^4" vary "^1" -cosmiconfig@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" - integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== +cosmiconfig@^7.0.1, cosmiconfig@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== dependencies: "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" @@ -2998,7 +3086,7 @@ css-blank-pseudo@^3.0.3: css-box-model@^1.2.0: version "1.2.1" - resolved "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== dependencies: tiny-invariant "^1.0.6" @@ -3072,9 +3160,9 @@ csstype@^2.6.8: integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA== csstype@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz" - integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA== + version "3.1.1" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== currently-unhandled@^0.4.1: version "0.4.1" @@ -3173,18 +3261,18 @@ dashdash@^1.12.0: data-uri-to-buffer@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== date-fns-tz@^1.1.4: - version "1.3.6" - resolved "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.6.tgz" - integrity sha512-C8q7mErvG4INw1ZwAFmPlGjEo5Sv4udjKVbTc03zpP9cu6cp5AemFzKhz0V68LGcWEtX5mJudzzg3G04emIxLA== + version "1.3.7" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.3.7.tgz#e8e9d2aaceba5f1cc0e677631563081fdcb0e69a" + integrity sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g== date-fns@^2.23.0: - version "2.29.2" - resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.29.2.tgz" - integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA== + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== dateformat@^5.0.3: version "5.0.3" @@ -3273,9 +3361,9 @@ define-properties@^1.1.3, define-properties@^1.1.4: object-keys "^1.1.1" defu@^6.0.0, defu@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.0.tgz#7a5411655da73335c7d933256911f17c74443e2d" - integrity sha512-pOFYRTIhoKujrmbTRhcW5lYQLBXw/dlTwfI8IguF1QCDJOcJzNH1w+YFjxqy6BAuJrClTy6MUE8q+oKJ2FLsIw== + version "6.1.1" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.1.tgz#a12c712349197c545dc61d3cd3b607b4cc7ef0c1" + integrity sha512-aA964RUCsBt0FGoNIlA3uFgo2hO+WWC0fiC6DBps/0SFzkKcYoM/3CzVLIa5xSsrFjdioMdYgAIbwo80qp2MoA== del@^6.0.0: version "6.1.1" @@ -3302,9 +3390,9 @@ denque@^2.0.1: integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== destr@^1.1.1, destr@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destr/-/destr-1.2.0.tgz#8adb17fa3861b8032ea1bba92f60ba989a9ff119" - integrity sha512-JG+cG4ZPB1L27sl2C2URg8MIOmIUtTbE5wEx02BpmrTCqg/hXxFKXsYsnODl5PdpqNRaS1KQGUQ56V8jk8XpYQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/destr/-/destr-1.2.1.tgz#03f2e7cbcd01f9190938d05718948de9d7dfb71a" + integrity sha512-ud8w0qMLlci6iFG7CNgeRr8OcbUWMsbfjtWft1eJ5Luqrz/M8Ebqk/KCzne8rKUlIQWWfLv0wD6QHrqOf4GshA== detect-browser@^5.2.0: version "5.3.0" @@ -3438,7 +3526,7 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5: +es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.4: version "1.20.4" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.4.tgz#1d103f9f8d78d4cf0713edcd6d0ed1a46eed5861" integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA== @@ -3500,11 +3588,11 @@ escape-string-regexp@^4.0.0: integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-config-next@^12.2.4: - version "12.3.2" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.3.2.tgz#3dd50a9ae5acf77f0ca5c94d77b604e40c67bf5c" - integrity sha512-ir8t3wRNexUMSKsMNh7J1EWwiBAr2dd015UAiCFvQ/01MRBPmFof1C6PFh/RqlupwamSZlv6j8gfaV1RxHuRoQ== + version "12.3.3" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.3.3.tgz#b04f6b55b43a72983e68e51329a993d2a8ea648c" + integrity sha512-ZqovaLqMlWQh9yVbqJ2gvOLk6acAZX4vRkORFsiI5lv9oJDDBbDDeTPG2KmpZ3K+l/wJ+xo6bm4FN90j94snhw== dependencies: - "@next/eslint-plugin-next" "12.3.2" + "@next/eslint-plugin-next" "12.3.3" "@rushstack/eslint-patch" "^1.1.3" "@typescript-eslint/parser" "^5.21.0" eslint-import-resolver-node "^0.3.6" @@ -3601,24 +3689,25 @@ eslint-plugin-react-hooks@^4.5.0: integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== eslint-plugin-react@^7.31.7: - version "7.31.10" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz#6782c2c7fe91c09e715d536067644bbb9491419a" - integrity sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA== + version "7.31.11" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.31.11.tgz#011521d2b16dcf95795df688a4770b4eaab364c8" + integrity sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw== dependencies: - array-includes "^3.1.5" - array.prototype.flatmap "^1.3.0" + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" + array.prototype.tosorted "^1.1.1" doctrine "^2.1.0" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.5" - object.fromentries "^2.0.5" - object.hasown "^1.1.1" - object.values "^1.1.5" + object.entries "^1.1.6" + object.fromentries "^2.0.6" + object.hasown "^1.1.2" + object.values "^1.1.6" prop-types "^15.8.1" resolve "^2.0.0-next.3" semver "^6.3.0" - string.prototype.matchall "^4.0.7" + string.prototype.matchall "^4.0.8" eslint-scope@^5.1.1: version "5.1.1" @@ -3871,7 +3960,7 @@ fastq@^1.6.0: fetch-blob@^3.1.2, fetch-blob@^3.1.4: version "3.2.0" - resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== dependencies: node-domexception "^1.0.0" @@ -3946,7 +4035,7 @@ form-data@~2.3.2: formdata-polyfill@^4.0.10: version "4.0.10" - resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== dependencies: fetch-blob "^3.1.2" @@ -4000,7 +4089,7 @@ fs-memo@^1.2.0: fs-minipass@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== dependencies: minipass "^3.0.0" @@ -4040,6 +4129,11 @@ functions-have-names@^1.2.2: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +generic-pool@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" + integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -4258,7 +4352,7 @@ has@^1.0.3: hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" - resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" @@ -4331,7 +4425,7 @@ immer@^9.0.12: import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" @@ -4659,9 +4753,9 @@ is-wsl@^2.2.0: is-docker "^2.0.0" isbot@^3.4.5: - version "3.6.2" - resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.6.2.tgz#aad0b8b44cd1bbb82822aac9498b2c6f3eafbce8" - integrity sha512-jk8IRbrvyTJC2f4VpxXQ4ff+xVJgsXHygtpc5Sl6m9PWuF8oXVcpEmcZKR88j5WOoqpHTxrOKccCpxdAgW7Afw== + version "3.6.5" + resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.6.5.tgz#a749980d9dfba9ebcc03ee7b548d1f24dd8c9f1e" + integrity sha512-BchONELXt6yMad++BwGpa0oQxo/uD0keL7N15cYVf0A1oMIoNQ79OqeYdPMFWDrNhCqCbRuw9Y9F3QBjvAxZ5g== isexe@^2.0.0: version "2.0.0" @@ -4835,9 +4929,9 @@ jws@^3.2.2: safe-buffer "^5.0.1" kafkajs@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.0.tgz" - integrity sha512-+sdgyLuC0Idw1g9LSBXjtoCr4K+vVaHP+tulzAK+V+HHvO3uW5woNkzLnbBx0MN4WRuEl/5g84M3FSkH0ZDzrA== + version "2.2.3" + resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-2.2.3.tgz#a1ab1b7c4a27699871a89b3978b5cfe5b05c6f3e" + integrity sha512-JmzIiLHE/TdQ7b4I2B/DNMtfhTh66fmEaEg7gGkyQXBC6f1A7I2jSjeUsVIJfC8d9YcEIURyBjtOEKBO5OHVhg== kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" @@ -4849,10 +4943,10 @@ kleur@^3.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -known-css-properties@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.25.0.tgz#6ebc4d4b412f602e5cfbeb4086bd544e34c0a776" - integrity sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA== +known-css-properties@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.26.0.tgz#008295115abddc045a9f4ed7e2a84dc8b3a77649" + integrity sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg== language-subtag-registry@~0.3.2: version "0.3.22" @@ -4913,9 +5007,9 @@ listhen@^0.2.15: ufo "^0.8.5" listhen@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/listhen/-/listhen-0.3.4.tgz#5bb0db65dae5f2204ffa7ee46b40f2330854cf7f" - integrity sha512-cuzWWoIWF8JvsPLmIurTkUXi27owH4RRKnBsbPswRJvB82uTv15W01yOOLaPvjxY5mMlftmW2p1XnxB835AdRA== + version "0.3.5" + resolved "https://registry.yarnpkg.com/listhen/-/listhen-0.3.5.tgz#04a0f6dbdab5bbac711992004a37c8306fad3e4e" + integrity sha512-suyt79hNmCFeBIyftcLqLPfYiXeB795gSUWOJT7nspl2IvREY0Q9xvchLhekxvQ0KiOPvWoyALnc9Mxoelm0Pw== dependencies: clipboardy "^3.0.0" colorette "^2.0.19" @@ -4924,7 +5018,7 @@ listhen@^0.3.4: http-shutdown "^1.2.2" ip-regex "^5.0.0" node-forge "^1.3.1" - ufo "^0.8.5" + ufo "^0.8.6" listr2@^3.12.2: version "3.14.0" @@ -5116,12 +5210,12 @@ mathml-tag-names@^2.1.3: integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== maxmind@^4.3.6: - version "4.3.6" - resolved "https://registry.npmjs.org/maxmind/-/maxmind-4.3.6.tgz" - integrity sha512-CwnEZqJX0T6b2rWrc0/V3n9hL/hWAMEn7fY09077YJUHiHx7cn/esA2ZIz8BpYLSJUf7cGVel0oUJa9jMwyQpg== + version "4.3.8" + resolved "https://registry.yarnpkg.com/maxmind/-/maxmind-4.3.8.tgz#e284edd3619987211ee45909076c6d4fcd2dc4df" + integrity sha512-HrfxEu5yPBPtTy/OT+W5bPQwEfLUX0EHqe2EbJiB47xQMumHqXvSP7PAwzV8Z++NRCmQwy4moQrTSt0+dH+Jmg== dependencies: mmdb-lib "2.0.2" - tiny-lru "8.0.2" + tiny-lru "9.0.3" mdn-data@2.0.14: version "2.0.14" @@ -5130,7 +5224,7 @@ mdn-data@2.0.14: "memoize-one@>=3.1.1 <6", memoize-one@^5.1.1: version "5.2.1" - resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== memorystream@^0.3.1: @@ -5250,15 +5344,15 @@ minimist@^1.2.5: integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== minipass@^3.0.0: - version "3.1.6" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz" - integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + version "3.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" + integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== dependencies: yallist "^4.0.0" minizlib@^2.1.1: version "2.1.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== dependencies: minipass "^3.0.0" @@ -5276,12 +5370,12 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== mmdb-lib@2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-2.0.2.tgz" + resolved "https://registry.yarnpkg.com/mmdb-lib/-/mmdb-lib-2.0.2.tgz#fe60404142c0456c19607c72caa15821731ae957" integrity sha512-shi1I+fCPQonhTi7qyb6hr7hi87R7YS69FlfJiMFuJ12+grx0JyL56gLNzGTYXPU7EhAPkMLliGeyHer0K+AVA== moize@^6.1.0: @@ -5293,9 +5387,9 @@ moize@^6.1.0: micro-memoize "^4.0.11" moment-timezone@^0.5.35: - version "0.5.38" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.38.tgz#9674a5397b8be7c13de820fd387d8afa0f725aad" - integrity sha512-nMIrzGah4+oYZPflDvLZUgoVUO4fvAqHstvG3xAUnMolWncuAiLDWNnJZj6EwJGMGfb1ZcuTFE6GI3hNOVWI/Q== + version "0.5.39" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.39.tgz#342625a3b98810f04c8f4ea917e448d3525e600b" + integrity sha512-hoB6suq4ISDj7BDgctiOy6zljBsdYT0++0ZzZm9rtxIvJhIbQ3nmbgSWe7dNFGurl6/7b1OUkHlmN9JWgXVz7w== dependencies: moment ">= 2.9.0" @@ -5354,30 +5448,30 @@ next-basics@^0.23.0: jsonwebtoken "^8.5.1" next@^12.3.1: - version "12.3.2" - resolved "https://registry.yarnpkg.com/next/-/next-12.3.2.tgz#3a3356a8d752726128825a8bdf17f2a3b3f861cf" - integrity sha512-orzvvebCwOqaz1eA5ZA0R5dbKxqtJyw7yeig7kDspu6p8OrplfyelzpvMHcDTKscv/l0nn/0l0v3mSsE8w4k7A== + version "12.3.3" + resolved "https://registry.yarnpkg.com/next/-/next-12.3.3.tgz#c1286fc24e378b0b0279ef205db7d8dd994dcd79" + integrity sha512-Rx2Y6Wl5R8E77NOfBupp/B9OPCklqfqD0yN2+rDivhMjd6hjVFH5n0WTDI4PWwDmZsdNcYt6NV85kJ3PLR+eNQ== dependencies: - "@next/env" "12.3.2" + "@next/env" "12.3.3" "@swc/helpers" "0.4.11" caniuse-lite "^1.0.30001406" postcss "8.4.14" styled-jsx "5.0.7" use-sync-external-store "1.2.0" optionalDependencies: - "@next/swc-android-arm-eabi" "12.3.2" - "@next/swc-android-arm64" "12.3.2" - "@next/swc-darwin-arm64" "12.3.2" - "@next/swc-darwin-x64" "12.3.2" - "@next/swc-freebsd-x64" "12.3.2" - "@next/swc-linux-arm-gnueabihf" "12.3.2" - "@next/swc-linux-arm64-gnu" "12.3.2" - "@next/swc-linux-arm64-musl" "12.3.2" - "@next/swc-linux-x64-gnu" "12.3.2" - "@next/swc-linux-x64-musl" "12.3.2" - "@next/swc-win32-arm64-msvc" "12.3.2" - "@next/swc-win32-ia32-msvc" "12.3.2" - "@next/swc-win32-x64-msvc" "12.3.2" + "@next/swc-android-arm-eabi" "12.3.3" + "@next/swc-android-arm64" "12.3.3" + "@next/swc-darwin-arm64" "12.3.3" + "@next/swc-darwin-x64" "12.3.3" + "@next/swc-freebsd-x64" "12.3.3" + "@next/swc-linux-arm-gnueabihf" "12.3.3" + "@next/swc-linux-arm64-gnu" "12.3.3" + "@next/swc-linux-arm64-musl" "12.3.3" + "@next/swc-linux-x64-gnu" "12.3.3" + "@next/swc-linux-x64-musl" "12.3.3" + "@next/swc-win32-arm64-msvc" "12.3.3" + "@next/swc-win32-ia32-msvc" "12.3.3" + "@next/swc-win32-x64-msvc" "12.3.3" nice-try@^1.0.4: version "1.0.5" @@ -5398,7 +5492,7 @@ node-addon-api@^5.0.0: node-domexception@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== node-fetch-native@^0.1.8: @@ -5414,9 +5508,9 @@ node-fetch@^2.0.0, node-fetch@^2.6.6: whatwg-url "^5.0.0" node-fetch@^3.2.8: - version "3.2.10" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz" - integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA== + version "3.3.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.0.tgz#37e71db4ecc257057af828d523a7243d651d91e4" + integrity sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA== dependencies: data-uri-to-buffer "^4.0.0" fetch-blob "^3.1.4" @@ -5526,50 +5620,50 @@ object.assign@^4.1.3, object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" - integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== +object.entries@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23" + integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" -object.fromentries@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251" - integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== +object.fromentries@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" + integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" -object.hasown@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.1.tgz#ad1eecc60d03f49460600430d97f23882cf592a3" - integrity sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A== +object.hasown@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92" + integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== dependencies: define-properties "^1.1.4" - es-abstract "^1.19.5" + es-abstract "^1.20.4" -object.values@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== +object.values@^1.1.5, object.values@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" ohmyfetch@^0.4.18, ohmyfetch@^0.4.19: - version "0.4.20" - resolved "https://registry.yarnpkg.com/ohmyfetch/-/ohmyfetch-0.4.20.tgz#735895d396c6d2fbf5705625db1196bd3f29be16" - integrity sha512-+c3/l+X91owrT1reTos1R13rb2j8NGZpKi0bRWwrnxIHlr1FZ8NzghIsNBKpUvk9nsnFoNK4phw+nTnXrcALzA== + version "0.4.21" + resolved "https://registry.yarnpkg.com/ohmyfetch/-/ohmyfetch-0.4.21.tgz#6850db751fc7bbf08153aa8b11ff1ef45fcfd963" + integrity sha512-VG7f/JRvqvBOYvL0tHyEIEG7XHWm7OqIfAs6/HqwWwDfjiJ1g0huIpe5sFEmyb+7hpFa1EGNH2aERWR72tlClw== dependencies: - destr "^1.1.1" + destr "^1.2.0" node-fetch-native "^0.1.8" ufo "^0.8.6" - undici "^5.11.0" + undici "^5.12.0" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -6020,7 +6114,15 @@ postcss-selector-not@^5.0.0: dependencies: balanced-match "^1.0.0" -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: +postcss-selector-parser@^6.0.10: + version "6.0.11" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" + integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: version "6.0.10" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz" integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== @@ -6051,10 +6153,10 @@ postcss@^8.3.11: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.12, postcss@^8.4.17: - version "8.4.18" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.18.tgz#6d50046ea7d3d66a85e0e782074e7203bc7fbca2" - integrity sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA== +postcss@^8.4.12, postcss@^8.4.19: + version "8.4.19" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc" + integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA== dependencies: nanoid "^3.3.4" picocolors "^1.0.0" @@ -6179,7 +6281,7 @@ radix3@^0.2.1: raf-schd@^4.0.2: version "4.0.3" - resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== randombytes@^2.1.0: @@ -6199,10 +6301,22 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-basics@^0.29.0: + version "0.29.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.29.0.tgz#f63deb74a9ddb4097752f0d7e1283fcaa69c35fe" + integrity sha512-cj3dlzDNZc3XUeqgwQl4QQ/5lEpAxgewHBg63F4jjY2Jpph4WoKdYj6bd1KFEGWifbDFFNZSLE39UWwC9gh+gg== + dependencies: + classnames "^2.3.1" + react "^18.2.0" + react-dom "^18.2.0" + react-hook-form "^7.34.2" + react-spring "^9.5.5" + react-window "^1.8.6" + react-beautiful-dnd@^13.1.0: - version "13.1.0" - resolved "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz" - integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== dependencies: "@babel/runtime" "^7.9.2" css-box-model "^1.2.0" @@ -6212,20 +6326,24 @@ react-beautiful-dnd@^13.1.0: redux "^4.0.4" use-memo-one "^1.1.1" -react-dom@^17.0.0: - version "17.0.2" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" - integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" - scheduler "^0.20.2" + scheduler "^0.23.0" react-fast-compare@^2.0.1: version "2.0.4" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== +react-hook-form@^7.34.2: + version "7.39.5" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.39.5.tgz#a4272b60288ef5e1bb42bbb6ba3b36d243ab2879" + integrity sha512-OE0HKyz5IPc6svN2wd+e+evidZrw4O4WZWAWYzQVZuHi+hYnHFSLnxOq0ddjbdmaLIsLHut/ab7j72y2QT3+KA== + react-intl@^5.24.7: version "5.25.1" resolved "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz" @@ -6244,18 +6362,18 @@ react-intl@^5.24.7: react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== react-is@^17.0.2: version "17.0.2" - resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== react-redux@^7.2.0: - version "7.2.8" - resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz" - integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw== + version "7.2.9" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" + integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== dependencies: "@babel/runtime" "^7.15.4" "@types/react-redux" "^7.1.20" @@ -6274,24 +6392,24 @@ react-simple-maps@^2.3.0: d3-zoom "^2.0.0" topojson-client "^3.1.0" -react-spring@^9.4.4: - version "9.5.2" - resolved "https://registry.npmjs.org/react-spring/-/react-spring-9.5.2.tgz" - integrity sha512-OGWNgKi2TSjpqsK67NCUspaCgEvWcG7HcpO9KAaDLFzFGNxWdGdN3YTXhhWUqCsLAx9I6LxPzmRuUPsMNqTgrw== +react-spring@^9.4.4, react-spring@^9.5.5: + version "9.5.5" + resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-9.5.5.tgz#314009a65efc04d0ef157d3d60590dbb9de65f3c" + integrity sha512-vMGVd2yjgxWcRCzoLn9AD1d24+WpunHBRg5DoehcRdiBocaOH6qgle0xN9C5LPplXfv4yIpS5QWGN5MKrWxSZg== dependencies: - "@react-spring/core" "~9.5.2" - "@react-spring/konva" "~9.5.2" - "@react-spring/native" "~9.5.2" - "@react-spring/three" "~9.5.2" - "@react-spring/web" "~9.5.2" - "@react-spring/zdog" "~9.5.2" + "@react-spring/core" "~9.5.5" + "@react-spring/konva" "~9.5.5" + "@react-spring/native" "~9.5.5" + "@react-spring/three" "~9.5.5" + "@react-spring/web" "~9.5.5" + "@react-spring/zdog" "~9.5.5" react-tooltip@^4.2.21: - version "4.2.21" - resolved "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.21.tgz" - integrity sha512-zSLprMymBDowknr0KVDiJ05IjZn9mQhhg4PRsqln0OZtURAJ1snt1xi5daZfagsh6vfsziZrc9pErPTDY1ACig== + version "4.5.0" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.5.0.tgz#862a39fbb05522624fb6efa782b245a89a0db784" + integrity sha512-mJNurq29atce+TJc9Xe+/FHrcEs3K9J7wkjZZXwbK5Yq6uG5SZeKSFHwd0wcRPUipVwx5crmgzSW8Zu1xyvLTQ== dependencies: - prop-types "^15.7.2" + prop-types "^15.8.1" uuid "^7.0.3" react-use-measure@^2.0.4: @@ -6302,20 +6420,19 @@ react-use-measure@^2.0.4: debounce "^1.2.1" react-window@^1.8.6: - version "1.8.7" - resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz" - integrity sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA== + version "1.8.8" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.8.tgz#1b52919f009ddf91970cbdb2050a6c7be44df243" + integrity sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ== dependencies: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" -react@^17.0.0: - version "17.0.2" - resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" read-babelrc-up@^1.1.0: version "1.1.0" @@ -6396,9 +6513,21 @@ redis-parser@^3.0.0: dependencies: redis-errors "^1.0.0" +redis@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.5.0.tgz#8a461c8718e380ea899ba3711aa0bb217b112089" + integrity sha512-oZGAmOKG+RPnHo0UxM5GGjJ0dBd/Vi4fs3MYwM1p2baDoXC0wpm0yOdpxVS9K+0hM84ycdysp2eHg2xGoQ4FEw== + dependencies: + "@redis/bloom" "1.1.0" + "@redis/client" "1.4.0" + "@redis/graph" "1.1.0" + "@redis/json" "1.0.4" + "@redis/search" "1.1.0" + "@redis/time-series" "1.0.4" + redux@^4.0.0, redux@^4.0.4: version "4.2.0" - resolved "https://registry.npmjs.org/redux/-/redux-4.2.0.tgz" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== dependencies: "@babel/runtime" "^7.9.2" @@ -6422,10 +6551,10 @@ regenerate@^1.4.0, regenerate@^1.4.2: resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.4: - version "0.13.10" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" - integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== +regenerator-runtime@^0.13.10: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regenerator-transform@^0.15.0: version "0.15.0" @@ -6439,7 +6568,7 @@ regexp-tree@^0.1.24: resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.24.tgz#3d6fa238450a4d66e5bc9c4c14bb720e2196829d" integrity sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw== -regexp.prototype.flags@^1.4.1, regexp.prototype.flags@^1.4.3: +regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== @@ -6659,13 +6788,12 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -scheduler@^0.20.2: - version "0.20.2" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz" - integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== dependencies: loose-envify "^1.1.0" - object-assign "^4.1.1" schema-utils@*: version "4.0.0" @@ -6715,20 +6843,13 @@ semver@^7.2.1: dependencies: lru-cache "^6.0.0" -semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: +semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: lru-cache "^6.0.0" -semver@^7.3.6: - version "7.3.7" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== - dependencies: - lru-cache "^6.0.0" - serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz" @@ -6952,18 +7073,18 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d" - integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg== +string.prototype.matchall@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" + integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" has-symbols "^1.0.3" internal-slot "^1.0.3" - regexp.prototype.flags "^1.4.1" + regexp.prototype.flags "^1.4.3" side-channel "^1.0.4" string.prototype.padend@^3.0.0: @@ -6976,22 +7097,22 @@ string.prototype.padend@^3.0.0: es-abstract "^1.19.1" string.prototype.trimend@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" - integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" - es-abstract "^1.19.5" + es-abstract "^1.20.4" string.prototype.trimstart@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" - integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== dependencies: call-bind "^1.0.2" define-properties "^1.1.4" - es-abstract "^1.19.5" + es-abstract "^1.20.4" string_decoder@^1.1.1: version "1.3.0" @@ -7066,9 +7187,9 @@ stylelint-config-css-modules@^4.1.0: stylelint-scss "^4.2.0" stylelint-config-prettier@^9.0.3: - version "9.0.3" - resolved "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-9.0.3.tgz" - integrity sha512-5n9gUDp/n5tTMCq1GLqSpA30w2sqWITSSEiAWQlpxkKGAUbjcemQ0nbkRvRUa0B1LgD3+hCvdL7B1eTxy1QHJg== + version "9.0.4" + resolved "https://registry.yarnpkg.com/stylelint-config-prettier/-/stylelint-config-prettier-9.0.4.tgz#1b1dda614d5b3ef6c1f583fa6fa55f88245eb00b" + integrity sha512-38nIGTGpFOiK5LjJ8Ma1yUgpKENxoKSOhbDNSemY7Ep0VsJoXIW9Iq/2hSt699oB9tReynfWicTAoIHiq8Rvbg== stylelint-config-recommended@^7.0.0: version "7.0.0" @@ -7087,14 +7208,14 @@ stylelint-scss@^4.2.0: postcss-value-parser "^4.1.0" stylelint@^14.5.3: - version "14.14.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-14.14.0.tgz#1acb52497c9a921f23f9c4014d4e0ee6eba768d0" - integrity sha512-yUI+4xXfPHVnueYddSQ/e1GuEA/2wVhWQbGj16AmWLtQJtn28lVxfS4b0CsWyVRPgd3Auzi0NXOthIEUhtQmmA== + version "14.15.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-14.15.0.tgz#4df55078e734869f81f6b85bbec2d56a4b478ece" + integrity sha512-JOgDAo5QRsqiOZPZO+B9rKJvBm64S0xasbuRPAbPs6/vQDgDCnZLIiw6XcAS6GQKk9k1sBWR6rmH3Mfj8OknKg== dependencies: "@csstools/selector-specificity" "^2.0.2" balanced-match "^2.0.0" colord "^2.9.3" - cosmiconfig "^7.0.1" + cosmiconfig "^7.1.0" css-functions-list "^3.1.0" debug "^4.3.4" fast-glob "^3.2.12" @@ -7108,13 +7229,13 @@ stylelint@^14.5.3: import-lazy "^4.0.0" imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.25.0" + known-css-properties "^0.26.0" mathml-tag-names "^2.1.3" meow "^9.0.0" micromatch "^4.0.5" normalize-path "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.17" + postcss "^8.4.19" postcss-media-query-parser "^0.2.3" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^6.0.0" @@ -7126,7 +7247,7 @@ stylelint@^14.5.3: style-search "^0.1.0" supports-hyperlinks "^2.3.0" svg-tags "^1.0.0" - table "^6.8.0" + table "^6.8.1" v8-compile-cache "^2.3.0" write-file-atomic "^4.0.2" @@ -7187,7 +7308,7 @@ svgo@^2.8.0: picocolors "^1.0.0" stable "^0.1.8" -table@^6.0.9, table@^6.8.0: +table@^6.0.9: version "6.8.0" resolved "https://registry.npmjs.org/table/-/table-6.8.0.tgz" integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== @@ -7198,6 +7319,17 @@ table@^6.0.9, table@^6.8.0: string-width "^4.2.3" strip-ansi "^6.0.1" +table@^6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" + integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" @@ -7220,9 +7352,9 @@ tar-stream@^2.1.4: readable-stream "^3.1.1" tar@^6.1.2: - version "6.1.11" - resolved "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + version "6.1.12" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6" + integrity sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -7272,14 +7404,14 @@ tiny-glob@^0.2.9: globrex "^0.1.2" tiny-invariant@^1.0.6: - version "1.2.0" - resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz" - integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== -tiny-lru@8.0.2: - version "8.0.2" - resolved "https://registry.npmjs.org/tiny-lru/-/tiny-lru-8.0.2.tgz" - integrity sha512-ApGvZ6vVvTNdsmt676grvCkUCGwzG9IqXma5Z07xJgiC5L7akUMof5U8G2JTI9Rz/ovtVhJBlY6mNhEvtjzOIg== +tiny-lru@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-9.0.3.tgz#f6a2121f433607a7f338881a23090829c1ea8cae" + integrity sha512-/i9GruRjXsnDgehxvy6iZ4AFNVxngEFbwzirhdulomMNPGPVV3ECMZOWSw0w4sRMZ9Al9m4jy08GPvRxRUGYlw== tiny-warning@^1.0.2: version "1.0.3" @@ -7441,7 +7573,7 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -undici@^5.11.0: +undici@^5.12.0: version "5.12.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.12.0.tgz#c758ffa704fbcd40d506e4948860ccaf4099f531" integrity sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg== @@ -7532,11 +7664,11 @@ uri-js@^4.2.2: punycode "^2.1.0" use-memo-one@^1.1.1: - version "1.1.2" - resolved "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz" - integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== -use-sync-external-store@1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -7553,7 +7685,7 @@ uuid@3.4.0, uuid@^3.3.2: uuid@^7.0.3: version "7.0.3" - resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== uuid@^8.3.2: @@ -7601,7 +7733,7 @@ vue@^3.2.23: web-streams-polyfill@^3.0.3: version "3.2.1" - resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== webidl-conversions@^3.0.0: @@ -7701,9 +7833,9 @@ write-json-file@^4.3.0: write-file-atomic "^3.0.0" ws@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.10.0.tgz#00a28c09dfb76eae4eb45c3b565f771d6951aa51" - integrity sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw== + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== xss@^1.0.14: version "1.0.14" @@ -7713,7 +7845,7 @@ xss@^1.0.14: commander "^2.20.3" cssfilter "0.0.10" -yallist@^4.0.0: +yallist@4.0.0, yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== From 3efd2a5b10585ecb9fc5396e9ccb1c6feed0896b Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 21 Nov 2022 22:32:59 -0800 Subject: [PATCH 19/38] Remove user/team transfer from website update. --- pages/api/websites/[id]/index.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 7b634e13..e525f237 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -15,8 +15,6 @@ export interface WebsiteRequestBody { name: string; domain: string; shareId: string; - userId?: string; - teamId?: string; } export default async ( @@ -39,14 +37,10 @@ export default async ( } if (req.method === 'POST') { - const { ...data } = req.body; - - if (data.userId && data.userId === null && data.teamId && data.teamId === null) { - badRequest(res, 'A website must be assigned to a User or Team.'); - } + const { name, domain, shareId } = req.body; try { - await updateWebsite(websiteId, data); + await updateWebsite(websiteId, { name, domain, shareId }); } catch (e: any) { if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { return serverError(res, 'That share ID is already taken.'); From 5da7a208a816d14c74540e2183b18a3ba776c55e Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 21 Nov 2022 22:51:23 -0800 Subject: [PATCH 20/38] delete website in relational query --- queries/admin/website.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/queries/admin/website.ts b/queries/admin/website.ts index 7ee64a35..ad742823 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -97,9 +97,7 @@ export async function deleteWebsite(websiteId: string) { }); } -async function deleteWebsiteRelationalQuery( - websiteId, -): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { +async function deleteWebsiteRelationalQuery(websiteId,): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; return transaction([ @@ -109,10 +107,7 @@ async function deleteWebsiteRelationalQuery( client.session.deleteMany({ where: { websiteId }, }), - client.website.update({ - data: { - isDeleted: true, - }, + client.website.delete({ where: { id: websiteId }, }), ]).then(async data => { From 9172098fa735d085885897e86b01635f83e9a4f9 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 21 Nov 2022 23:23:16 -0800 Subject: [PATCH 21/38] Fix login secure token creation. --- pages/api/auth/login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/auth/login.ts b/pages/api/auth/login.ts index b7458a7a..e23415a9 100644 --- a/pages/api/auth/login.ts +++ b/pages/api/auth/login.ts @@ -42,7 +42,7 @@ export default async ( await redis.set(key, user); - const token = createSecureToken(key, secret()); + const token = createSecureToken({ key }, secret()); return ok(res, { token, user }); } From d85b2be5f8a4dc6d360faf6a53126b11c368a62b Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 22 Nov 2022 15:06:52 -0800 Subject: [PATCH 22/38] Add event type to event. --- pages/api/collect.ts | 16 ++++++++++++---- pages/api/websites/[id]/index.ts | 4 ++-- queries/analytics/event/saveEvent.ts | 5 +++-- queries/analytics/pageview/savePageView.ts | 5 +++-- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/pages/api/collect.ts b/pages/api/collect.ts index 2aa2bde1..254689fe 100644 --- a/pages/api/collect.ts +++ b/pages/api/collect.ts @@ -29,6 +29,18 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { return unauthorized(res); } + const { type, payload } = getJsonBody(req); + + const { referrer, event_name: eventName, event_data: eventData } = payload; + let { url } = payload; + + // Validate eventData is JSON + const valid = eventData && typeof eventData === 'object' && !Array.isArray(eventData); + + if (!valid) { + return badRequest(res, 'Event Data must be in the form of a JSON Object.'); + } + const ignoreIps = process.env.IGNORE_IP; const ignoreHostnames = process.env.IGNORE_HOSTNAME; @@ -75,10 +87,6 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { const session = req.session; - const { type, payload } = getJsonBody(req); - - let { url, referrer, event_name: eventName, event_data: eventData } = payload; - if (process.env.REMOVE_TRAILING_SLASH) { url = url.replace(/\/$/, ''); } diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index e525f237..356feb0a 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -4,7 +4,7 @@ import { allowQuery } from 'lib/auth'; import { UmamiApi } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, serverError, unauthorized, badRequest } from 'next-basics'; +import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; import { deleteWebsite, getWebsite, updateWebsite } from 'queries'; export interface WebsiteRequestQuery { @@ -19,7 +19,7 @@ export interface WebsiteRequestBody { export default async ( req: NextApiRequestQueryBody, - res: NextApiResponse, + res: NextApiResponse, ) => { await useCors(req, res); await useAuth(req, res); diff --git a/queries/analytics/event/saveEvent.ts b/queries/analytics/event/saveEvent.ts index 521f6c28..c2a92d10 100644 --- a/queries/analytics/event/saveEvent.ts +++ b/queries/analytics/event/saveEvent.ts @@ -72,11 +72,12 @@ async function clickhouseQuery(data: { const { getDateFormat, sendMessage } = kafka; const website = await cache.fetchWebsite(websiteId); - const params = { + const message = { website_id: websiteId, session_id: sessionId, event_id: uuid(), url: url?.substring(0, URL_LENGTH), + event_type: UmamiApi.EventType.Event, event_name: eventName?.substring(0, EVENT_NAME_LENGTH), event_data: eventData ? JSON.stringify(eventData) : null, rev_id: website?.revId || 0, @@ -85,7 +86,7 @@ async function clickhouseQuery(data: { ...args, }; - await sendMessage(params, 'event'); + await sendMessage(message, 'event'); return data; } diff --git a/queries/analytics/pageview/savePageView.ts b/queries/analytics/pageview/savePageView.ts index fd3f7e81..b73b8c0a 100644 --- a/queries/analytics/pageview/savePageView.ts +++ b/queries/analytics/pageview/savePageView.ts @@ -50,7 +50,7 @@ async function clickhouseQuery(data) { const website = await cache.fetchWebsite(websiteId); const { getDateFormat, sendMessage } = kafka; - const msg = { + const message = { session_id: sessionId, website_id: websiteId, url: url?.substring(0, URL_LENGTH), @@ -58,10 +58,11 @@ async function clickhouseQuery(data) { rev_id: website?.revId || 0, created_at: getDateFormat(new Date()), country: country ? country : null, + event_type: UmamiApi.EventType.Pageview, ...args, }; - await sendMessage(msg, 'event'); + await sendMessage(message, 'event'); return data; } From 502facd426c59648b92c815eb28bc105258dda60 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 22 Nov 2022 22:17:49 -0800 Subject: [PATCH 23/38] Allow user to be added to team with role. --- pages/api/teams/[id]/user.ts | 18 +++++++++--------- queries/admin/teamUser.ts | 31 +++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/pages/api/teams/[id]/user.ts b/pages/api/teams/[id]/user.ts index 8a0bb98b..76e60217 100644 --- a/pages/api/teams/[id]/user.ts +++ b/pages/api/teams/[id]/user.ts @@ -1,18 +1,18 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { allowQuery } from 'lib/auth'; import { UmamiApi } from 'lib/constants'; -import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createTeamUser, deleteTeamUser, getUsersByTeamId, getTeamUser } from 'queries'; +import { createTeamUser, deleteTeamUser, getUser, getUsersByTeamId } from 'queries'; export interface TeamUserRequestQuery { id: string; } export interface TeamUserRequestBody { - user_id: string; + email: string; + role_id: string; team_user_id?: string; } @@ -39,16 +39,16 @@ export default async ( return unauthorized(res, 'You must be the owner of this team.'); } - const { user_id: userId } = req.body; + const { email, role_id: roleId } = req.body; - // Check for TeamUser - const teamUser = getTeamUser({ userId, teamId }); + // Check for User + const user = await getUser({ username: email }); - if (!teamUser) { - return badRequest(res, 'The User already exists on this Team.'); + if (!user) { + return badRequest(res, 'The User does not exists.'); } - const updated = await createTeamUser({ id: uuid(), userId, teamId }); + const updated = await createTeamUser(user.id, teamId, roleId); return ok(res, updated); } diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts index 3bbe9a76..0adf506f 100644 --- a/queries/admin/teamUser.ts +++ b/queries/admin/teamUser.ts @@ -1,12 +1,31 @@ -import { Prisma, TeamUser } from '@prisma/client'; +import { Prisma, TeamUser, UserRole } from '@prisma/client'; +import { uuid } from 'lib/crypto'; import prisma from 'lib/prisma'; export async function createTeamUser( - data: Prisma.TeamUserCreateInput | Prisma.TeamUserUncheckedCreateInput, -): Promise { - return prisma.client.teamUser.create({ - data, - }); + userId: string, + teamId: string, + roleId: string, +): Promise<[TeamUser, UserRole]> { + const { client } = prisma; + + return client.$transaction([ + client.teamUser.create({ + data: { + id: uuid(), + userId, + teamId, + }, + }), + client.userRole.create({ + data: { + id: uuid(), + userId, + teamId, + roleId, + }, + }), + ]); } export async function getTeamUser(where: Prisma.TeamUserWhereInput): Promise { From fe01574b1602e83796330b49f9f9a503b74c50e5 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 28 Nov 2022 11:14:24 -0800 Subject: [PATCH 24/38] Updated login form. --- components/declarations.d.ts | 2 ++ components/forms/LoginForm.js | 11 +++-------- next-env.d.ts | 10 +++++----- next.config.js | 2 +- pages/_app.js | 8 ++++---- 5 files changed, 15 insertions(+), 18 deletions(-) create mode 100644 components/declarations.d.ts diff --git a/components/declarations.d.ts b/components/declarations.d.ts new file mode 100644 index 00000000..31e44ff3 --- /dev/null +++ b/components/declarations.d.ts @@ -0,0 +1,2 @@ +declare module '*.css'; +declare module '*.svg'; diff --git a/components/forms/LoginForm.js b/components/forms/LoginForm.js index 894bc9b9..aad3f97d 100644 --- a/components/forms/LoginForm.js +++ b/components/forms/LoginForm.js @@ -1,4 +1,3 @@ -import { useRef } from 'react'; import { useMutation } from '@tanstack/react-query'; import { Form, @@ -20,19 +19,15 @@ export default function LoginForm() { const router = useRouter(); const { post } = useApi(); const { mutate, error, isLoading } = useMutation(data => post('/auth/login', data)); - const ref = useRef(); const handleSubmit = async data => { mutate(data, { - onSuccess: async ({ token, account }) => { + onSuccess: async ({ token, user }) => { setAuthToken(token); - setUser(account); + setUser(user); await router.push('/websites'); }, - onError: async () => { - ref.current.reset(undefined, { keepDirty: true, keepValues: true }); - }, }); }; @@ -44,7 +39,7 @@ export default function LoginForm() {

umami

-
+ diff --git a/next-env.d.ts b/next-env.d.ts index 62b8a52d..4f11a03d 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,5 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js index a1dbb4e8..eac47415 100644 --- a/next.config.js +++ b/next.config.js @@ -49,7 +49,7 @@ module.exports = { webpack(config) { config.module.rules.push({ test: /\.svg$/, - issuer: /\.js$/, + issuer: /\.{js|jsx|ts|tsx}$/, use: ['@svgr/webpack'], }); diff --git a/pages/_app.js b/pages/_app.js index fb4e8e44..15ab7245 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -2,6 +2,7 @@ import Head from 'next/head'; import { useRouter } from 'next/router'; import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Container } from 'react-basics'; import useLocale from 'hooks/useLocale'; import useConfig from 'hooks/useConfig'; import 'react-basics/dist/styles.css'; @@ -12,9 +13,8 @@ import '@fontsource/inter/600.css'; const client = new QueryClient(); export default function App({ Component, pageProps }) { - const { locale, messages } = useLocale(); + const { locale, messages, dir } = useLocale(); const { basePath } = useRouter(); - const { dir } = useLocale(); useConfig(); const Wrapper = ({ children }) => {children}; @@ -38,9 +38,9 @@ export default function App({ Component, pageProps }) { -
+ -
+ ); From d5437c895e0313f69cc77c73bfc19a9aed25ce74 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 28 Nov 2022 13:08:48 -0800 Subject: [PATCH 25/38] Add Role to TeamUser. --- db/postgresql/schema.prisma | 12 ++++++------ queries/admin/permission.ts | 21 +++++++++++++++++++++ queries/admin/teamUser.ts | 31 ++++++++++--------------------- queries/admin/userRole.ts | 3 +-- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 962d9a15..49661d92 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -140,6 +140,7 @@ model Role { groupRoles GroupRole[] userRoles UserRole[] RolePermission RolePermission[] + TeamUser TeamUser[] @@map("role") } @@ -162,15 +163,13 @@ model UserRole { id String @id() @unique() @map("user_role_id") @db.Uuid roleId String @map("role_id") @db.Uuid userId String @map("user_id") @db.Uuid - teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - role Role @relation(fields: [roleId], references: [id]) - user User @relation(fields: [userId], references: [id]) - team Team? @relation(fields: [teamId], references: [id]) + role Role @relation(fields: [roleId], references: [id]) + user User @relation(fields: [userId], references: [id]) - @@unique([roleId, userId, teamId]) + @@unique([roleId, userId]) @@map("user_role") } @@ -181,7 +180,6 @@ model Team { isDeleted Boolean @default(false) @map("is_deleted") teamUsers TeamUser[] - UserRole UserRole[] Website Website[] @@map("team") @@ -191,12 +189,14 @@ model TeamUser { id String @id() @unique() @map("team_user_id") @db.Uuid teamId String @map("team_id") @db.Uuid userId String @map("user_id") @db.Uuid + roleId String @map("role_id") @db.Uuid isOwner Boolean @default(false) @map("is_owner") createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") team Team @relation(fields: [teamId], references: [id]) user User @relation(fields: [userId], references: [id]) + role Role @relation(fields: [roleId], references: [id]) @@map("team_user") } diff --git a/queries/admin/permission.ts b/queries/admin/permission.ts index 9c98065c..9667114a 100644 --- a/queries/admin/permission.ts +++ b/queries/admin/permission.ts @@ -40,6 +40,27 @@ export async function getPermissionsByUserId(userId, name?: string): Promise { + return prisma.client.permission.findMany({ + where: { + ...(name ? { name } : {}), + RolePermission: { + every: { + role: { + is: { + TeamUser: { + every: { + teamId, + }, + }, + }, + }, + }, + }, + }, + }); +} + export async function updatePermission( data: Prisma.PermissionUpdateInput, where: Prisma.PermissionWhereUniqueInput, diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts index 0adf506f..a0160560 100644 --- a/queries/admin/teamUser.ts +++ b/queries/admin/teamUser.ts @@ -1,4 +1,4 @@ -import { Prisma, TeamUser, UserRole } from '@prisma/client'; +import { Prisma, TeamUser } from '@prisma/client'; import { uuid } from 'lib/crypto'; import prisma from 'lib/prisma'; @@ -6,26 +6,15 @@ export async function createTeamUser( userId: string, teamId: string, roleId: string, -): Promise<[TeamUser, UserRole]> { - const { client } = prisma; - - return client.$transaction([ - client.teamUser.create({ - data: { - id: uuid(), - userId, - teamId, - }, - }), - client.userRole.create({ - data: { - id: uuid(), - userId, - teamId, - roleId, - }, - }), - ]); +): Promise { + return prisma.client.teamUser.create({ + data: { + id: uuid(), + userId, + teamId, + roleId, + }, + }); } export async function getTeamUser(where: Prisma.TeamUserWhereInput): Promise { diff --git a/queries/admin/userRole.ts b/queries/admin/userRole.ts index b93b1042..a5c24d8f 100644 --- a/queries/admin/userRole.ts +++ b/queries/admin/userRole.ts @@ -21,11 +21,10 @@ export async function getUserRoles(where: Prisma.UserRoleWhereInput): Promise { +export async function getUserRolesByUserId(userId: string): Promise { return prisma.client.userRole.findMany({ where: { userId, - teamId, }, }); } From 77b739870eb37d85052fa555f36a3663d70124ab Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Tue, 29 Nov 2022 21:56:43 -0800 Subject: [PATCH 26/38] Add database migration. --- .../migrations/01_init/migration.sql | 332 ++++++++++++++++++ db/postgresql/migrations/migration_lock.toml | 3 + db/postgresql/schema.prisma | 3 - lib/auth.ts | 14 +- lib/constants.ts | 14 + 5 files changed, 360 insertions(+), 6 deletions(-) create mode 100644 db/postgresql/migrations/01_init/migration.sql create mode 100644 db/postgresql/migrations/migration_lock.toml diff --git a/db/postgresql/migrations/01_init/migration.sql b/db/postgresql/migrations/01_init/migration.sql new file mode 100644 index 00000000..2ab8f8c3 --- /dev/null +++ b/db/postgresql/migrations/01_init/migration.sql @@ -0,0 +1,332 @@ +-- CreateTable +CREATE TABLE "user" ( + "user_id" UUID NOT NULL, + "username" VARCHAR(255) NOT NULL, + "password" VARCHAR(60) NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "user_pkey" PRIMARY KEY ("user_id") +); + +-- CreateTable +CREATE TABLE "session" ( + "session_id" UUID NOT NULL, + "website_id" UUID NOT NULL, + "hostname" VARCHAR(100), + "browser" VARCHAR(20), + "os" VARCHAR(20), + "device" VARCHAR(20), + "screen" VARCHAR(11), + "language" VARCHAR(35), + "country" CHAR(2), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "session_pkey" PRIMARY KEY ("session_id") +); + +-- CreateTable +CREATE TABLE "website" ( + "website_id" UUID NOT NULL, + "name" VARCHAR(100) NOT NULL, + "domain" VARCHAR(500), + "share_id" VARCHAR(64), + "rev_id" INTEGER NOT NULL DEFAULT 0, + "user_id" UUID, + "team_id" UUID, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "website_pkey" PRIMARY KEY ("website_id") +); + +-- CreateTable +CREATE TABLE "website_event" ( + "event_id" UUID NOT NULL, + "website_id" UUID NOT NULL, + "session_id" UUID NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "url" VARCHAR(500) NOT NULL, + "referrer" VARCHAR(500), + "event_type" INTEGER NOT NULL DEFAULT 1, + "event_name" VARCHAR(50), + "event_data" JSONB, + + CONSTRAINT "website_event_pkey" PRIMARY KEY ("event_id") +); + +-- CreateTable +CREATE TABLE "group" ( + "group_id" UUID NOT NULL, + "name" VARCHAR(255) NOT NULL, + "description" VARCHAR(255), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "group_pkey" PRIMARY KEY ("group_id") +); + +-- CreateTable +CREATE TABLE "group_role" ( + "group_role_id" UUID NOT NULL, + "group_id" UUID NOT NULL, + "role_id" UUID NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + "userId" UUID, + + CONSTRAINT "group_role_pkey" PRIMARY KEY ("group_role_id") +); + +-- CreateTable +CREATE TABLE "group_user" ( + "group_user_id" UUID NOT NULL, + "group_id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "group_user_pkey" PRIMARY KEY ("group_user_id") +); + +-- CreateTable +CREATE TABLE "permission" ( + "permission_id" UUID NOT NULL, + "name" VARCHAR(255) NOT NULL, + "description" VARCHAR(255), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "permission_pkey" PRIMARY KEY ("permission_id") +); + +-- CreateTable +CREATE TABLE "role" ( + "role_id" UUID NOT NULL, + "name" VARCHAR(255) NOT NULL, + "description" VARCHAR(255), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "role_pkey" PRIMARY KEY ("role_id") +); + +-- CreateTable +CREATE TABLE "role_permission" ( + "role_permission_id" UUID NOT NULL, + "role_id" UUID NOT NULL, + "permission_id" UUID NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "role_permission_pkey" PRIMARY KEY ("role_permission_id") +); + +-- CreateTable +CREATE TABLE "user_role" ( + "user_role_id" UUID NOT NULL, + "role_id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "user_role_pkey" PRIMARY KEY ("user_role_id") +); + +-- CreateTable +CREATE TABLE "team" ( + "team_id" UUID NOT NULL, + "name" VARCHAR(50) NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "team_pkey" PRIMARY KEY ("team_id") +); + +-- CreateTable +CREATE TABLE "team_user" ( + "team_user_id" UUID NOT NULL, + "team_id" UUID NOT NULL, + "user_id" UUID NOT NULL, + "role_id" UUID NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "is_deleted" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "team_user_pkey" PRIMARY KEY ("team_user_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_user_id_key" ON "user"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_username_key" ON "user"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "session_session_id_key" ON "session"("session_id"); + +-- CreateIndex +CREATE INDEX "session_created_at_idx" ON "session"("created_at"); + +-- CreateIndex +CREATE INDEX "session_website_id_idx" ON "session"("website_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "website_website_id_key" ON "website"("website_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "website_share_id_key" ON "website"("share_id"); + +-- CreateIndex +CREATE INDEX "website_created_at_idx" ON "website"("created_at"); + +-- CreateIndex +CREATE INDEX "website_share_id_idx" ON "website"("share_id"); + +-- CreateIndex +CREATE INDEX "website_event_created_at_idx" ON "website_event"("created_at"); + +-- CreateIndex +CREATE INDEX "website_event_session_id_idx" ON "website_event"("session_id"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_idx" ON "website_event"("website_id"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_created_at_idx" ON "website_event"("website_id", "created_at"); + +-- CreateIndex +CREATE INDEX "website_event_website_id_session_id_created_at_idx" ON "website_event"("website_id", "session_id", "created_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "group_group_id_key" ON "group"("group_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "group_name_key" ON "group"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "group_role_group_role_id_key" ON "group_role"("group_role_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "group_user_group_user_id_key" ON "group_user"("group_user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "permission_permission_id_key" ON "permission"("permission_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "permission_name_key" ON "permission"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "role_role_id_key" ON "role"("role_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "role_name_key" ON "role"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "role_permission_role_permission_id_key" ON "role_permission"("role_permission_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "role_permission_role_id_permission_id_key" ON "role_permission"("role_id", "permission_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_role_user_role_id_key" ON "user_role"("user_role_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "user_role_role_id_user_id_key" ON "user_role"("role_id", "user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "team_team_id_key" ON "team"("team_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "team_name_key" ON "team"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "team_user_team_user_id_key" ON "team_user"("team_user_id"); + +-- AddForeignKey +ALTER TABLE "website" ADD CONSTRAINT "website_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "team"("team_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "website" ADD CONSTRAINT "website_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("user_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_role" ADD CONSTRAINT "group_role_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "group"("group_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_role" ADD CONSTRAINT "group_role_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "role"("role_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_role" ADD CONSTRAINT "group_role_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("user_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_user" ADD CONSTRAINT "group_user_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "group"("group_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "group_user" ADD CONSTRAINT "group_user_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "role_permission" ADD CONSTRAINT "role_permission_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "role"("role_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "role_permission" ADD CONSTRAINT "role_permission_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "permission"("permission_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_role" ADD CONSTRAINT "user_role_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "role"("role_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_role" ADD CONSTRAINT "user_role_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "team_user" ADD CONSTRAINT "team_user_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "team"("team_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "team_user" ADD CONSTRAINT "team_user_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "team_user" ADD CONSTRAINT "team_user_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "role"("role_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Add System User +INSERT INTO "user" (user_id, username, password) VALUES ('41e2b680-648e-4b09-bcd7-3e2b10c06264' ,'admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa'); + +-- Add Roles +INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Admin', 'System Admin.'); +INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Member', 'Create and maintain websites.'); +INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Owner', 'Create and maintain the team, memberships, websites, and responsible for billing.'); +INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Member', 'Create and maintain websites.'); +INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Guest', 'View Websites.'); + +-- Add Permissions +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'admin', 'System Admin'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'website:create', 'Create website'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'website:read', 'Read website'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'website:update', 'Update website'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'website:delete', 'Delete website'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'website:reset', 'Reset website'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'team:create', 'Create team'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'team:update', 'Update team'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'team:delete', 'Delete team'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'team:add-user', 'Add team member'); +INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'team:remove-user', 'Remove team member'); + +-- Add Permissions to Roles +INSERT INTO "role_permission" +SELECT gen_random_uuid(), "role_id", "permission_id" +FROM "role" r JOIN "permission" p ON 0 = 0 +WHERE r.name = 'Admin' AND p.name = 'admin'; + +INSERT INTO "role_permission" +SELECT gen_random_uuid(), "role_id", "permission_id" +FROM "role" r JOIN "permission" p ON 0 = 0 +WHERE r.name = 'Member' AND p.name in ('website:create', 'website:read', 'website:update', 'website:delete', 'website:reset', 'team:create'); + +INSERT INTO "role_permission" +SELECT gen_random_uuid(), "role_id", "permission_id" +FROM "role" r JOIN "permission" p ON 0 = 0 +WHERE r.name = 'Team Owner' AND p.name in ('website:create', 'website:read', 'website:update', 'website:delete', 'website:reset', 'team:create', 'team:update', 'team:delete', 'team:add-user', 'team:remove-user'); + +INSERT INTO "role_permission" +SELECT gen_random_uuid(), "role_id", "permission_id" +FROM "role" r JOIN "permission" p ON 0 = 0 +WHERE r.name = 'Team Member' AND p.name in ('website:create', 'website:read', 'website:update', 'website:delete', 'website:reset'); + +INSERT INTO "role_permission" +SELECT gen_random_uuid(), "role_id", "permission_id" +FROM "role" r JOIN "permission" p ON 0 = 0 +WHERE r.name = 'Team Guest' AND p.name in ('website:read'); \ No newline at end of file diff --git a/db/postgresql/migrations/migration_lock.toml b/db/postgresql/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/db/postgresql/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 49661d92..7d478716 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -124,7 +124,6 @@ model Permission { name String @unique() @db.VarChar(255) description String? @db.VarChar(255) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") RolePermission RolePermission[] @@map("permission") @@ -135,7 +134,6 @@ model Role { name String @unique() @db.VarChar(255) description String? @db.VarChar(255) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") groupRoles GroupRole[] userRoles UserRole[] @@ -190,7 +188,6 @@ model TeamUser { teamId String @map("team_id") @db.Uuid userId String @map("user_id") @db.Uuid roleId String @map("role_id") @db.Uuid - isOwner Boolean @default(false) @map("is_owner") createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") diff --git a/lib/auth.ts b/lib/auth.ts index 7035df69..21225982 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -34,6 +34,13 @@ export function parseShareToken(req) { } } +export function hasPermission( + value: UmamiApi.Role | UmamiApi.Permission, + permissions: UmamiApi.Role[] | UmamiApi.Permission[], +) { + return permissions.some(a => a === value); +} + export function isValidToken(token, validation) { try { if (typeof validation === 'object') { @@ -85,7 +92,6 @@ export async function allowQuery( const teamUser = await getTeamUser({ userId: user.id, teamId: typeId ?? id, - isDeleted: false, }); return teamUser; @@ -93,10 +99,12 @@ export async function allowQuery( const teamUser = await getTeamUser({ userId: user.id, teamId: typeId ?? id, - isDeleted: false, }); - return teamUser && teamUser.isOwner; + return ( + teamUser && + (teamUser.roleId === UmamiApi.Role.TeamOwner || teamUser.roleId === UmamiApi.Role.Admin) + ); } } diff --git a/lib/constants.ts b/lib/constants.ts index dbdf4e38..bea2cc0a 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -14,10 +14,24 @@ export namespace UmamiApi { export enum Permission { Admin = 'Admin', + WebsiteCreate = 'website:create', + WebsiteRead = 'website:read', + WebsiteUpdate = 'website:update', + WebsiteReset = 'website:reset', + WebsiteDelete = 'website:delete', + TeamCreate = 'team:create', + TeamUpdate = 'team:update', + TeamDelete = 'team:delete', + TeamAddUser = 'team:add-user', + TeamRemoveUser = 'team:remove-user', } export enum Role { Admin = 'Admin', + Member = 'Member', + TeamOwner = 'Team Owner', + TeamMember = 'Team Member', + TeamGuest = 'Team Guest,', } } export const CURRENT_VERSION = process.env.currentVersion; From a4e80ca3e564e86261fd7d52af977f536a805e91 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 1 Dec 2022 10:58:50 -0800 Subject: [PATCH 27/38] Refactored permissions check. Updated redis lib. --- lib/auth.ts | 20 ++++++++++++++++++++ lib/cache.ts | 4 ++-- lib/constants.ts | 24 +++++++++++++++++++++--- lib/redis.js | 10 ++++++---- pages/api/websites/[id]/index.ts | 15 +++++++++------ 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/lib/auth.ts b/lib/auth.ts index 21225982..67b9b57c 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -120,3 +120,23 @@ export async function checkPermission(req: NextApiRequestAuth, type: UmamiApi.Pe return userRole.length > 0; } + +export async function canViewWebsite(userId: string, websiteId: string) { + const website = await cache.fetchWebsite(websiteId); + + if (website.userId) { + return userId === website.userId; + } + + return false; +} + +export async function canUpdateWebsite(userId: string, websiteId: string) { + const website = await cache.fetchWebsite(websiteId); + + if (website.userId) { + return userId === website.userId; + } + + return false; +} diff --git a/lib/cache.ts b/lib/cache.ts index 0cf6e2c8..5c79def0 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -1,5 +1,5 @@ import { User, Website } from '@prisma/client'; -import redis, { DELETED } from 'lib/redis'; +import redis from 'lib/redis'; import { getSession, getUser, getWebsite } from '../queries'; async function fetchObject(key, query) { @@ -23,7 +23,7 @@ async function storeObject(key, data) { } async function deleteObject(key) { - return redis.set(key, DELETED); + return redis.set(key, redis.DELETED); } async function fetchWebsite(id): Promise { diff --git a/lib/constants.ts b/lib/constants.ts index bea2cc0a..456fbf55 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -34,6 +34,27 @@ export namespace UmamiApi { TeamGuest = 'Team Guest,', } } + +export const PERMISSIONS = { + all: 'all', + websiteCreate: 'website:create', + websiteUpdate: 'website:update', + websiteDelete: 'website:delete', + teamCreate: 'team:create', + teamUpdate: 'team:update', + teamDelete: 'team:delete', +}; + +export const ROLES = { + admin: { name: 'admin', permissions: [PERMISSIONS.all] }, + teamOwner: { name: 'team-owner', permissions: [PERMISSIONS.teamUpdate, PERMISSIONS.teamDelete] }, + teamMember: { + name: 'team-member', + permissions: [PERMISSIONS.websiteCreate, PERMISSIONS.websiteUpdate, PERMISSIONS.websiteDelete], + }, + teamGuest: { name: 'team-guest' }, +}; + export const CURRENT_VERSION = process.env.currentVersion; export const AUTH_TOKEN = 'umami.auth'; export const LOCALE_CONFIG = 'umami.locale'; @@ -57,9 +78,6 @@ export const DEFAULT_WEBSITE_LIMIT = 10; export const REALTIME_RANGE = 30; export const REALTIME_INTERVAL = 3000; -export const TYPE_WEBSITE = 'website'; -export const TYPE_USER = 'user'; - export const THEME_COLORS = { light: { primary: '#2680eb', diff --git a/lib/redis.js b/lib/redis.js index 5ec4147d..fe236feb 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -3,16 +3,18 @@ import debug from 'debug'; const log = debug('umami:redis'); const REDIS = Symbol(); +const DELETED = 'DELETED'; let redis; -const enabled = Boolean(process.env.REDIS_URL); +const url = process.env.REDIS_URL; +const enabled = Boolean(url); async function getClient() { - if (!process.env.REDIS_URL) { + if (!enabled) { return null; } - const client = createClient({ url: process.env.REDIS_URL }); + const client = createClient({ url }); client.on('error', err => log(err)); await client.connect(); @@ -59,4 +61,4 @@ async function connect() { return redis; } -export default { enabled, client: redis, log, connect, get, set, del }; +export default { enabled, client: redis, log, connect, get, set, del, DELETED }; diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 356feb0a..85b04148 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -1,7 +1,6 @@ import { Website } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite, canUpdateWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; @@ -26,17 +25,21 @@ export default async ( const { id: websiteId } = req.query; - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { - return unauthorized(res); - } - if (req.method === 'GET') { + if (!(await canViewWebsite(req.auth.user.id, websiteId))) { + return unauthorized(res); + } + const website = await getWebsite({ id: websiteId }); return ok(res, website); } if (req.method === 'POST') { + if (!(await canUpdateWebsite(req.auth.user.id, websiteId))) { + return unauthorized(res); + } + const { name, domain, shareId } = req.body; try { From 06bebadbb9007c12cb65463e15e148c47e9de8bb Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 1 Dec 2022 20:53:37 -0800 Subject: [PATCH 28/38] Feat/um 114 roles and permissions (#1683) * Auth checkpoint. * Merge branch 'dev' into feat/um-114-roles-and-permissions --- .../migrations/01_init/migration.sql | 8 +- db/postgresql/schema.prisma | 95 +-------- lib/auth.ts | 181 ++++++++++-------- lib/constants.ts | 80 ++++---- pages/api/teams/[id]/index.ts | 13 +- pages/api/teams/[id]/user.ts | 12 +- pages/api/teams/[id]/website.ts | 8 +- pages/api/teams/index.ts | 13 +- pages/api/users/[id]/index.ts | 21 +- pages/api/users/[id]/password.ts | 14 +- pages/api/users/[id]/role.ts | 25 ++- pages/api/users/index.ts | 17 +- pages/api/websites/[id]/active.ts | 12 +- pages/api/websites/[id]/eventdata.ts | 12 +- pages/api/websites/[id]/events.ts | 12 +- pages/api/websites/[id]/index.ts | 13 +- pages/api/websites/[id]/metrics.ts | 35 ++-- pages/api/websites/[id]/pageviews.ts | 36 ++-- pages/api/websites/[id]/reset.ts | 8 +- pages/api/websites/[id]/stats.ts | 32 ++-- pages/api/websites/index.ts | 7 +- queries/admin/permission.ts | 83 -------- queries/admin/role.ts | 57 ------ queries/admin/teamUser.ts | 4 +- queries/admin/user.ts | 4 +- queries/admin/website.ts | 9 +- queries/index.js | 2 - 27 files changed, 331 insertions(+), 482 deletions(-) delete mode 100644 queries/admin/permission.ts delete mode 100644 queries/admin/role.ts diff --git a/db/postgresql/migrations/01_init/migration.sql b/db/postgresql/migrations/01_init/migration.sql index 2ab8f8c3..620a8468 100644 --- a/db/postgresql/migrations/01_init/migration.sql +++ b/db/postgresql/migrations/01_init/migration.sql @@ -287,10 +287,10 @@ INSERT INTO "user" (user_id, username, password) VALUES ('41e2b680-648e-4b09-bcd -- Add Roles INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Admin', 'System Admin.'); -INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Member', 'Create and maintain websites.'); -INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Owner', 'Create and maintain the team, memberships, websites, and responsible for billing.'); -INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Member', 'Create and maintain websites.'); -INSERT INTO "role" ("role_id", "name", "description") (gen_random_uuid(), 'Team Guest', 'View Websites.'); +INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Member', 'Create and maintain websites.'); +INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Team Owner', 'Create and maintain the team, memberships, websites, and responsible for billing.'); +INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Team Member', 'Create and maintain websites.'); +INSERT INTO "role" ("role_id", "name", "description") VALUES (gen_random_uuid(), 'Team Guest', 'View Websites.'); -- Add Permissions INSERT INTO "permission" ("permission_id", "name", "description") VALUES (gen_random_uuid(), 'admin', 'System Admin'); diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 7d478716..7ef3dd88 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -14,11 +14,9 @@ model User { createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - groupRole GroupRole[] - groupUser GroupUser[] - userRole UserRole[] - teamUser TeamUser[] - Website Website[] + userRole UserRole[] + teamUser TeamUser[] + Website Website[] @@map("user") } @@ -78,96 +76,16 @@ model WebsiteEvent { @@map("website_event") } -model Group { - id String @id() @unique() @map("group_id") @db.Uuid - name String @unique() @db.VarChar(255) - description String? @db.VarChar(255) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") - - groupRoles GroupRole[] - groupUsers GroupUser[] - - @@map("group") -} - -model GroupRole { - id String @id() @unique() @map("group_role_id") @db.Uuid - groupId String @map("group_id") @db.Uuid - roleId String @map("role_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") - - group Group @relation(fields: [groupId], references: [id]) - role Role @relation(fields: [roleId], references: [id]) - user User? @relation(fields: [userId], references: [id]) - userId String? @db.Uuid - - @@map("group_role") -} - -model GroupUser { - id String @id() @unique() @map("group_user_id") @db.Uuid - groupId String @map("group_id") @db.Uuid - userId String @map("user_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") - - group Group @relation(fields: [groupId], references: [id]) - user User @relation(fields: [userId], references: [id]) - - @@map("group_user") -} - -model Permission { - id String @id() @unique() @map("permission_id") @db.Uuid - name String @unique() @db.VarChar(255) - description String? @db.VarChar(255) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - RolePermission RolePermission[] - - @@map("permission") -} - -model Role { - id String @id() @unique() @map("role_id") @db.Uuid - name String @unique() @db.VarChar(255) - description String? @db.VarChar(255) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - - groupRoles GroupRole[] - userRoles UserRole[] - RolePermission RolePermission[] - TeamUser TeamUser[] - - @@map("role") -} - -model RolePermission { - id String @id() @unique() @map("role_permission_id") @db.Uuid - roleId String @map("role_id") @db.Uuid - permissionId String @map("permission_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") - - role Role @relation(fields: [roleId], references: [id]) - permission Permission @relation(fields: [permissionId], references: [id]) - - @@unique([roleId, permissionId]) - @@map("role_permission") -} - model UserRole { id String @id() @unique() @map("user_role_id") @db.Uuid - roleId String @map("role_id") @db.Uuid + role String @map("role") @db.VarChar(100) userId String @map("user_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") - role Role @relation(fields: [roleId], references: [id]) user User @relation(fields: [userId], references: [id]) - @@unique([roleId, userId]) + @@unique([role, userId]) @@map("user_role") } @@ -187,13 +105,12 @@ model TeamUser { id String @id() @unique() @map("team_user_id") @db.Uuid teamId String @map("team_id") @db.Uuid userId String @map("user_id") @db.Uuid - roleId String @map("role_id") @db.Uuid + role String @map("role") @db.VarChar(100) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) isDeleted Boolean @default(false) @map("is_deleted") team Team @relation(fields: [teamId], references: [id]) user User @relation(fields: [userId], references: [id]) - role Role @relation(fields: [roleId], references: [id]) @@map("team_user") } diff --git a/lib/auth.ts b/lib/auth.ts index 67b9b57c..abcb6c49 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,10 +1,10 @@ +import { UserRole } from '@prisma/client'; import debug from 'debug'; -import { NextApiRequestAuth } from 'interface/api/nextApi'; import cache from 'lib/cache'; import { SHARE_TOKEN_HEADER, UmamiApi } from 'lib/constants'; import { secret } from 'lib/crypto'; import { parseSecureToken, parseToken } from 'next-basics'; -import { getPermissionsByUserId, getTeamUser, getUser } from 'queries'; +import { getTeamUser, getUserRoles } from 'queries'; const log = debug('umami:auth'); @@ -34,13 +34,6 @@ export function parseShareToken(req) { } } -export function hasPermission( - value: UmamiApi.Role | UmamiApi.Permission, - permissions: UmamiApi.Role[] | UmamiApi.Permission[], -) { - return permissions.some(a => a === value); -} - export function isValidToken(token, validation) { try { if (typeof validation === 'object') { @@ -56,71 +49,6 @@ export function isValidToken(token, validation) { return false; } -export async function allowQuery( - req: NextApiRequestAuth, - type: UmamiApi.AuthType, - typeId?: string, -) { - const { id } = req.query as { id: string }; - - const { user, shareToken } = req.auth; - - if (shareToken) { - return isValidToken(shareToken, { id }); - } - - if (user?.id) { - if (type === UmamiApi.AuthType.Website) { - const website = await cache.fetchWebsite(typeId ?? id); - - if (website && website.userId === user.id) { - return true; - } - - if (website.teamId) { - const teamUser = getTeamUser({ userId: user.id, teamId: website.teamId, isDeleted: false }); - - return teamUser; - } - - return false; - } else if (type === UmamiApi.AuthType.User) { - const user = await getUser({ id }); - - return user && user.id === id; - } else if (type === UmamiApi.AuthType.Team) { - const teamUser = await getTeamUser({ - userId: user.id, - teamId: typeId ?? id, - }); - - return teamUser; - } else if (type === UmamiApi.AuthType.TeamOwner) { - const teamUser = await getTeamUser({ - userId: user.id, - teamId: typeId ?? id, - }); - - return ( - teamUser && - (teamUser.roleId === UmamiApi.Role.TeamOwner || teamUser.roleId === UmamiApi.Role.Admin) - ); - } - } - - return false; -} - -export async function checkPermission(req: NextApiRequestAuth, type: UmamiApi.Permission) { - const { - user: { id: userId }, - } = req.auth; - - const userRole = await getPermissionsByUserId(userId, type); - - return userRole.length > 0; -} - export async function canViewWebsite(userId: string, websiteId: string) { const website = await cache.fetchWebsite(websiteId); @@ -128,7 +56,13 @@ export async function canViewWebsite(userId: string, websiteId: string) { return userId === website.userId; } - return false; + if (website.teamId) { + const teamUser = await getTeamUser({ userId, teamId: website.teamId }); + + checkPermission(UmamiApi.Permission.websiteUpdate, teamUser.role as keyof UmamiApi.Roles); + } + + return checkAdmin(userId); } export async function canUpdateWebsite(userId: string, websiteId: string) { @@ -138,5 +72,100 @@ export async function canUpdateWebsite(userId: string, websiteId: string) { return userId === website.userId; } - return false; + if (website.teamId) { + const teamUser = await getTeamUser({ userId, teamId: website.teamId }); + + checkPermission(UmamiApi.Permission.websiteUpdate, teamUser.role as keyof UmamiApi.Roles); + } + + return checkAdmin(userId); +} + +export async function canDeleteWebsite(userId: string, websiteId: string) { + const website = await cache.fetchWebsite(websiteId); + + if (website.userId) { + return userId === website.userId; + } + + if (website.teamId) { + const teamUser = await getTeamUser({ userId, teamId: website.teamId }); + + if (checkPermission(UmamiApi.Permission.websiteDelete, teamUser.role as keyof UmamiApi.Roles)) { + return true; + } + } + + return checkAdmin(userId); +} + +// To-do: Implement when payments are setup. +export async function canCreateTeam(userId: string) { + return !!userId; +} + +// To-do: Implement when payments are setup. +export async function canViewTeam(userId: string, teamId) { + const teamUser = await getTeamUser({ userId, teamId }); + return !!teamUser; +} + +export async function canUpdateTeam(userId: string, teamId: string) { + const teamUser = await getTeamUser({ userId, teamId }); + + if (checkPermission(UmamiApi.Permission.teamUpdate, teamUser.role as keyof UmamiApi.Roles)) { + return true; + } +} + +export async function canDeleteTeam(userId: string, teamId: string) { + const teamUser = await getTeamUser({ userId, teamId }); + + if (checkPermission(UmamiApi.Permission.teamDelete, teamUser.role as keyof UmamiApi.Roles)) { + return true; + } +} + +export async function canCreateUser(userId: string) { + return checkAdmin(userId); +} + +export async function canViewUser(userId: string, viewedUserId: string) { + if (userId === viewedUserId) { + return true; + } + + return checkAdmin(userId); +} + +export async function canViewUsers(userId: string) { + return checkAdmin(userId); +} + +export async function canUpdateUser(userId: string, viewedUserId: string) { + if (userId === viewedUserId) { + return true; + } + + return checkAdmin(userId); +} + +export async function canUpdateUserRole(userId: string) { + return checkAdmin(userId); +} + +export async function canDeleteUser(userId: string) { + return checkAdmin(userId); +} + +export async function checkPermission(permission: UmamiApi.Permission, role: keyof UmamiApi.Roles) { + return UmamiApi.Roles[role].permissions.some(a => a === permission); +} + +export async function checkAdmin(userId: string, userRoles?: UserRole[]) { + if (!userRoles) { + userRoles = await getUserRoles({ userId }); + } + + return userRoles.some(a => a.role === UmamiApi.Role.Admin); } diff --git a/lib/constants.ts b/lib/constants.ts index 456fbf55..0da17d21 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -9,52 +9,56 @@ export namespace UmamiApi { Website, User, Team, - TeamOwner, } export enum Permission { - Admin = 'Admin', - WebsiteCreate = 'website:create', - WebsiteRead = 'website:read', - WebsiteUpdate = 'website:update', - WebsiteReset = 'website:reset', - WebsiteDelete = 'website:delete', - TeamCreate = 'team:create', - TeamUpdate = 'team:update', - TeamDelete = 'team:delete', - TeamAddUser = 'team:add-user', - TeamRemoveUser = 'team:remove-user', + all = 'all', + websiteCreate = 'website:create', + websiteUpdate = 'website:update', + websiteDelete = 'website:delete', + teamCreate = 'team:create', + teamUpdate = 'team:update', + teamDelete = 'team:delete', } export enum Role { - Admin = 'Admin', - Member = 'Member', - TeamOwner = 'Team Owner', - TeamMember = 'Team Member', - TeamGuest = 'Team Guest,', + Admin = 'admin', + User = 'user', + TeamOwner = 'team-owner', + TeamMember = 'team-member', + TeamGuest = 'team-guest', } + + export const Roles = { + admin: { name: Role.Admin, permissions: [Permission.all] }, + member: { + name: Role.User, + permissions: [ + Permission.websiteCreate, + Permission.websiteUpdate, + Permission.websiteDelete, + Permission.teamCreate, + ], + }, + teamOwner: { + name: Role.TeamOwner, + permissions: [ + Permission.teamUpdate, + Permission.teamDelete, + Permission.websiteCreate, + Permission.websiteUpdate, + Permission.websiteDelete, + ], + }, + teamMember: { + name: Role.TeamMember, + permissions: [Permission.websiteCreate, Permission.websiteUpdate, Permission.websiteDelete], + }, + teamGuest: { name: Role.TeamGuest, permissions: [] }, + }; + + export type Roles = typeof Roles; } - -export const PERMISSIONS = { - all: 'all', - websiteCreate: 'website:create', - websiteUpdate: 'website:update', - websiteDelete: 'website:delete', - teamCreate: 'team:create', - teamUpdate: 'team:update', - teamDelete: 'team:delete', -}; - -export const ROLES = { - admin: { name: 'admin', permissions: [PERMISSIONS.all] }, - teamOwner: { name: 'team-owner', permissions: [PERMISSIONS.teamUpdate, PERMISSIONS.teamDelete] }, - teamMember: { - name: 'team-member', - permissions: [PERMISSIONS.websiteCreate, PERMISSIONS.websiteUpdate, PERMISSIONS.websiteDelete], - }, - teamGuest: { name: 'team-guest' }, -}; - export const CURRENT_VERSION = process.env.currentVersion; export const AUTH_TOKEN = 'umami.auth'; export const LOCALE_CONFIG = 'umami.locale'; diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index e93621eb..65a344c5 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -1,7 +1,6 @@ import { Team } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canDeleteTeam, canUpdateTeam, canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -21,12 +20,16 @@ export default async ( ) => { await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: teamId } = req.query; if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { + if (await canViewTeam(userId, teamId)) { return unauthorized(res); } + const user = await getTeam({ id: teamId }); return ok(res, user); @@ -35,7 +38,7 @@ export default async ( if (req.method === 'POST') { const { name } = req.body; - if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { + if (await canUpdateTeam(userId, teamId)) { return unauthorized(res, 'You must be the owner of this team.'); } @@ -45,7 +48,7 @@ export default async ( } if (req.method === 'DELETE') { - if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { + if (await canDeleteTeam(userId, teamId)) { return unauthorized(res, 'You must be the owner of this team.'); } diff --git a/pages/api/teams/[id]/user.ts b/pages/api/teams/[id]/user.ts index 76e60217..8b6aaf30 100644 --- a/pages/api/teams/[id]/user.ts +++ b/pages/api/teams/[id]/user.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canUpdateTeam, canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -22,10 +21,13 @@ export default async ( ) => { await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: teamId } = req.query; if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { + if (await canViewTeam(userId, teamId)) { return unauthorized(res); } @@ -35,7 +37,7 @@ export default async ( } if (req.method === 'POST') { - if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { + if (await canUpdateTeam(userId, teamId)) { return unauthorized(res, 'You must be the owner of this team.'); } @@ -54,7 +56,7 @@ export default async ( } if (req.method === 'DELETE') { - if (!(await allowQuery(req, UmamiApi.AuthType.TeamOwner))) { + if (await canUpdateTeam(userId, teamId)) { return unauthorized(res, 'You must be the owner of this team.'); } const { team_user_id } = req.body; diff --git a/pages/api/teams/[id]/website.ts b/pages/api/teams/[id]/website.ts index 364fc5da..3d959e70 100644 --- a/pages/api/teams/[id]/website.ts +++ b/pages/api/teams/[id]/website.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewTeam } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -21,10 +20,13 @@ export default async ( ) => { await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: teamId } = req.query; if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Team))) { + if (await canViewTeam(userId, teamId)) { return unauthorized(res); } diff --git a/pages/api/teams/index.ts b/pages/api/teams/index.ts index 5e8f6f4f..2831d07d 100644 --- a/pages/api/teams/index.ts +++ b/pages/api/teams/index.ts @@ -1,9 +1,10 @@ import { Team } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { canCreateTeam } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; -import { badRequest, methodNotAllowed, ok } from 'next-basics'; +import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createTeam, getTeam, getTeamsByUserId } from 'queries'; export interface TeamsRequestBody { name: string; @@ -17,16 +18,20 @@ export default async ( await useAuth(req, res); const { - user: { id }, + user: { id: userId }, } = req.auth; if (req.method === 'GET') { - const teams = await getTeamsByUserId(id); + const teams = await getTeamsByUserId(userId); return ok(res, teams); } if (req.method === 'POST') { + if (await canCreateTeam(userId)) { + return unauthorized(res); + } + const { name } = req.body; const team = await getTeam({ name }); @@ -36,7 +41,7 @@ export default async ( } const created = await createTeam({ - id: id || uuid(), + id: uuid(), name, }); diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index 8eef949b..d5215f23 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { checkPermission } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canDeleteUser, canUpdateUser, canViewUser, checkAdmin } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -27,7 +26,7 @@ export default async ( const { id } = req.query; if (req.method === 'GET') { - if (id !== userId) { + if (await canViewUser(userId, id)) { return unauthorized(res); } @@ -37,12 +36,12 @@ export default async ( } if (req.method === 'POST') { - const { username, password } = req.body; - - if (id !== userId) { + if (await canUpdateUser(userId, id)) { return unauthorized(res); } + const { username, password } = req.body; + const user = await getUser({ id }); const data: any = {}; @@ -52,7 +51,7 @@ export default async ( } // Only admin can change these fields - if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { + if (username && (await checkAdmin(userId))) { data.username = username; } @@ -71,12 +70,12 @@ export default async ( } if (req.method === 'DELETE') { - if (id === userId) { - return badRequest(res, 'You cannot delete your own user.'); + if (canDeleteUser(userId)) { + return unauthorized(res); } - if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { - return unauthorized(res); + if (id === userId) { + return badRequest(res, 'You cannot delete your own user.'); } await deleteUser(id); diff --git a/pages/api/users/[id]/password.ts b/pages/api/users/[id]/password.ts index e0024eda..5727cfbd 100644 --- a/pages/api/users/[id]/password.ts +++ b/pages/api/users/[id]/password.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canUpdateUser } from 'lib/auth'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { @@ -30,12 +29,15 @@ export default async ( const { current_password, new_password } = req.body; const { id } = req.query; - - if (!(await allowQuery(req, UmamiApi.AuthType.User))) { - return unauthorized(res); - } + const { + user: { id: userId }, + } = req.auth; if (req.method === 'POST') { + if (canUpdateUser(userId, id)) { + return unauthorized(res); + } + const user = await getUser({ id }); if (!checkPassword(current_password, user.password)) { diff --git a/pages/api/users/[id]/role.ts b/pages/api/users/[id]/role.ts index fe3dcbd4..fd4d33a6 100644 --- a/pages/api/users/[id]/role.ts +++ b/pages/api/users/[id]/role.ts @@ -1,20 +1,17 @@ import { UserRole } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { checkPermission } from 'lib/auth'; +import { canUpdateUserRole } from 'lib/auth'; import { UmamiApi } from 'lib/constants'; -import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { createUserRole, deleteUserRole, getUserRole, getUserRoles } from 'queries'; +import { deleteUserRole, getUserRole, getUserRoles, updateUserRole } from 'queries'; export interface UserRoleRequestQuery { id: string; } - export interface UserRoleRequestBody { - roleId: string; - teamId?: string; + role: UmamiApi.Role; userRoleId?: string; } @@ -29,7 +26,7 @@ export default async ( } = req.auth; const { id } = req.query; - if (id !== userId || !(await checkPermission(req, UmamiApi.Permission.Admin))) { + if (await canUpdateUserRole(userId)) { return unauthorized(res); } @@ -40,17 +37,17 @@ export default async ( } if (req.method === 'POST') { - const { roleId, teamId } = req.body; + const { role } = req.body; - const userRole = getUserRole({ userId: id, roleId, teamId }); + const userRole = await getUserRole({ userId: id }); - if (userRole) { + if (userRole && userRole.role === role) { return badRequest(res, 'Role already exists for User.'); + } else { + const updated = await updateUserRole({ role }, { id: userRole.id }); + + return ok(res, updated); } - - const updated = await createUserRole({ id: uuid(), userId: id, roleId, teamId }); - - return ok(res, updated); } if (req.method === 'DELETE') { diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 5f15c44a..b64c51ac 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { checkPermission } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canCreateUser, canViewUsers } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; @@ -19,17 +18,25 @@ export default async ( ) => { await useAuth(req, res); - if (!(await checkPermission(req, UmamiApi.Permission.Admin))) { - return unauthorized(res); - } + const { + user: { id: userId }, + } = req.auth; if (req.method === 'GET') { + if (canViewUsers(userId)) { + return unauthorized(res); + } + const users = await getUsers(); return ok(res, users); } if (req.method === 'POST') { + if (canCreateUser(userId)) { + return unauthorized(res); + } + const { username, password, id } = req.body; const user = await getUser({ username }); diff --git a/pages/api/websites/[id]/active.ts b/pages/api/websites/[id]/active.ts index 3323ad7b..66b02d1d 100644 --- a/pages/api/websites/[id]/active.ts +++ b/pages/api/websites/[id]/active.ts @@ -1,7 +1,6 @@ import { WebsiteActive } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -18,13 +17,16 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; + const { id: websiteId } = req.query; + if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (await canViewWebsite(userId, websiteId)) { return unauthorized(res); } - const { id: websiteId } = req.query; - const result = await getActiveVisitors(websiteId); return ok(res, result); diff --git a/pages/api/websites/[id]/eventdata.ts b/pages/api/websites/[id]/eventdata.ts index 120f61cc..e9df136f 100644 --- a/pages/api/websites/[id]/eventdata.ts +++ b/pages/api/websites/[id]/eventdata.ts @@ -1,7 +1,6 @@ import { WebsiteMetric } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -26,13 +25,16 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; + const { id: websiteId } = req.query; + if (req.method === 'POST') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (canViewWebsite(userId, websiteId)) { return unauthorized(res); } - const { id: websiteId } = req.query; - const { start_at, end_at, event_name: eventName, columns, filters } = req.body; const startDate = new Date(+start_at); diff --git a/pages/api/websites/[id]/events.ts b/pages/api/websites/[id]/events.ts index 832cb727..c2c6a8cc 100644 --- a/pages/api/websites/[id]/events.ts +++ b/pages/api/websites/[id]/events.ts @@ -1,7 +1,6 @@ import { WebsiteMetric } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import moment from 'moment-timezone'; import { NextApiResponse } from 'next'; @@ -27,13 +26,16 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; + const { id: websiteId, start_at, end_at, unit, tz, url, event_name } = req.query; + if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (canViewWebsite(userId, websiteId)) { return unauthorized(res); } - const { id: websiteId, start_at, end_at, unit, tz, url, event_name } = req.query; - if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) { return badRequest(res); } diff --git a/pages/api/websites/[id]/index.ts b/pages/api/websites/[id]/index.ts index 85b04148..9274d953 100644 --- a/pages/api/websites/[id]/index.ts +++ b/pages/api/websites/[id]/index.ts @@ -1,6 +1,6 @@ import { Website } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { canViewWebsite, canUpdateWebsite } from 'lib/auth'; +import { canViewWebsite, canUpdateWebsite, canDeleteWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics'; @@ -23,10 +23,13 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: websiteId } = req.query; if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth.user.id, websiteId))) { + if (!(await canViewWebsite(userId, websiteId))) { return unauthorized(res); } @@ -36,7 +39,7 @@ export default async ( } if (req.method === 'POST') { - if (!(await canUpdateWebsite(req.auth.user.id, websiteId))) { + if (!(await canUpdateWebsite(userId, websiteId))) { return unauthorized(res); } @@ -54,6 +57,10 @@ export default async ( } if (req.method === 'DELETE') { + if (!(await canDeleteWebsite(userId, websiteId))) { + return unauthorized(res); + } + await deleteWebsite(websiteId); return ok(res); diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index d3cdbf07..e84ff1fe 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -1,7 +1,7 @@ import { WebsiteMetric } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { FILTER_IGNORED, UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; +import { FILTER_IGNORED } from 'lib/constants'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -56,24 +56,27 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; + const { + id: websiteId, + type, + start_at, + end_at, + url, + referrer, + os, + browser, + device, + country, + } = req.query; + if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (!(await canViewWebsite(userId, websiteId))) { return unauthorized(res); } - const { - id: websiteId, - type, - start_at, - end_at, - url, - referrer, - os, - browser, - device, - country, - } = req.query; - const startDate = new Date(+start_at); const endDate = new Date(+end_at); diff --git a/pages/api/websites/[id]/pageviews.ts b/pages/api/websites/[id]/pageviews.ts index 208a052a..37ad891f 100644 --- a/pages/api/websites/[id]/pageviews.ts +++ b/pages/api/websites/[id]/pageviews.ts @@ -1,7 +1,6 @@ import { WebsitePageviews } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import moment from 'moment-timezone'; import { NextApiResponse } from 'next'; @@ -32,25 +31,28 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; + const { + id: websiteId, + start_at, + end_at, + unit, + tz, + url, + referrer, + os, + browser, + device, + country, + } = req.query; + if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (!(await canViewWebsite(userId, websiteId))) { return unauthorized(res); } - const { - id: websiteId, - start_at, - end_at, - unit, - tz, - url, - referrer, - os, - browser, - device, - country, - } = req.query; - const startDate = new Date(+start_at); const endDate = new Date(+end_at); diff --git a/pages/api/websites/[id]/reset.ts b/pages/api/websites/[id]/reset.ts index a5473ee6..d6c049df 100644 --- a/pages/api/websites/[id]/reset.ts +++ b/pages/api/websites/[id]/reset.ts @@ -1,6 +1,5 @@ import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -17,10 +16,13 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; const { id: websiteId } = req.query; if (req.method === 'POST') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (!(await canViewWebsite(userId, websiteId))) { return unauthorized(res); } diff --git a/pages/api/websites/[id]/stats.ts b/pages/api/websites/[id]/stats.ts index 2122a2da..f326db30 100644 --- a/pages/api/websites/[id]/stats.ts +++ b/pages/api/websites/[id]/stats.ts @@ -1,7 +1,6 @@ import { WebsiteStats } from 'interface/api/models'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; -import { allowQuery } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { canViewWebsite } from 'lib/auth'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -27,23 +26,26 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { + user: { id: userId }, + } = req.auth; + const { + id: websiteId, + start_at, + end_at, + url, + referrer, + os, + browser, + device, + country, + } = req.query; + if (req.method === 'GET') { - if (!(await allowQuery(req, UmamiApi.AuthType.Website))) { + if (!(await canViewWebsite(userId, websiteId))) { return unauthorized(res); } - const { - id: websiteId, - start_at, - end_at, - url, - referrer, - os, - browser, - device, - country, - } = req.query; - const startDate = new Date(+start_at); const endDate = new Date(+end_at); diff --git a/pages/api/websites/index.ts b/pages/api/websites/index.ts index 85da6c10..8e4408bc 100644 --- a/pages/api/websites/index.ts +++ b/pages/api/websites/index.ts @@ -1,12 +1,11 @@ import { Prisma } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; +import { checkAdmin } from 'lib/auth'; import { uuid } from 'lib/crypto'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok } from 'next-basics'; import { createWebsite, getAllWebsites, getWebsitesByUserId } from 'queries'; -import { checkPermission } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; export interface WebsitesRequestQuery { include_all?: boolean; @@ -33,7 +32,7 @@ export default async ( if (req.method === 'GET') { const { include_all } = req.query; - const isAdmin = await checkPermission(req, UmamiApi.Permission.Admin); + const isAdmin = await checkAdmin(userId); const websites = isAdmin && include_all ? await getAllWebsites() : await getWebsitesByUserId(userId); @@ -44,7 +43,7 @@ export default async ( if (req.method === 'POST') { const { name, domain, shareId, teamId } = req.body; - const data: Prisma.WebsiteCreateInput = { + const data: Prisma.WebsiteUncheckedCreateInput = { id: uuid(), name, domain, diff --git a/queries/admin/permission.ts b/queries/admin/permission.ts deleted file mode 100644 index 9667114a..00000000 --- a/queries/admin/permission.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Prisma, Permission } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function createPermission(data: Prisma.PermissionCreateInput): Promise { - return prisma.client.permission.create({ - data, - }); -} - -export async function getPermission(where: Prisma.PermissionWhereUniqueInput): Promise { - return prisma.client.permission.findUnique({ - where, - }); -} - -export async function getPermissions(where: Prisma.PermissionWhereInput): Promise { - return prisma.client.permission.findMany({ - where, - }); -} - -export async function getPermissionsByUserId(userId, name?: string): Promise { - return prisma.client.permission.findMany({ - where: { - ...(name ? { name } : {}), - RolePermission: { - every: { - role: { - is: { - userRoles: { - every: { - userId, - }, - }, - }, - }, - }, - }, - }, - }); -} - -export async function getPermissionsByTeamId(teamId, name?: string): Promise { - return prisma.client.permission.findMany({ - where: { - ...(name ? { name } : {}), - RolePermission: { - every: { - role: { - is: { - TeamUser: { - every: { - teamId, - }, - }, - }, - }, - }, - }, - }, - }); -} - -export async function updatePermission( - data: Prisma.PermissionUpdateInput, - where: Prisma.PermissionWhereUniqueInput, -): Promise { - return prisma.client.permission.update({ - data, - where, - }); -} - -export async function deletePermission(permissionId: string): Promise { - return prisma.client.permission.update({ - data: { - isDeleted: true, - }, - where: { - id: permissionId, - }, - }); -} diff --git a/queries/admin/role.ts b/queries/admin/role.ts deleted file mode 100644 index 2bf39930..00000000 --- a/queries/admin/role.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Prisma, Role } from '@prisma/client'; -import prisma from 'lib/prisma'; - -export async function createRole(data: { - id: string; - name: string; - description: string; -}): Promise { - return prisma.client.role.create({ - data, - }); -} - -export async function getRole(where: Prisma.RoleWhereUniqueInput): Promise { - return prisma.client.role.findUnique({ - where, - }); -} - -export async function getRoles(where: Prisma.RoleWhereInput): Promise { - return prisma.client.role.findMany({ - where, - }); -} - -export async function getRolesByUserId(userId: string): Promise { - return prisma.client.role.findMany({ - where: { - userRoles: { - every: { - userId, - }, - }, - }, - }); -} - -export async function updateRole( - data: Prisma.RoleUpdateInput, - where: Prisma.RoleWhereUniqueInput, -): Promise { - return prisma.client.role.update({ - data, - where, - }); -} - -export async function deleteRole(roleId: string): Promise { - return prisma.client.role.update({ - data: { - isDeleted: true, - }, - where: { - id: roleId, - }, - }); -} diff --git a/queries/admin/teamUser.ts b/queries/admin/teamUser.ts index a0160560..e88fa5e1 100644 --- a/queries/admin/teamUser.ts +++ b/queries/admin/teamUser.ts @@ -5,14 +5,14 @@ import prisma from 'lib/prisma'; export async function createTeamUser( userId: string, teamId: string, - roleId: string, + role: string, ): Promise { return prisma.client.teamUser.create({ data: { id: uuid(), userId, teamId, - roleId, + role, }, }); } diff --git a/queries/admin/user.ts b/queries/admin/user.ts index 7122323b..81727b16 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -36,9 +36,7 @@ export async function getUser( id: true, username: true, userRole: { - include: { - role: true, - }, + select: { role: true }, }, password: includePassword, }, diff --git a/queries/admin/website.ts b/queries/admin/website.ts index ad742823..c2ed683b 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -19,7 +19,10 @@ export async function createWebsite( }); } -export async function updateWebsite(websiteId, data: Prisma.WebsiteUpdateInput): Promise { +export async function updateWebsite( + websiteId, + data: Prisma.WebsiteUpdateInput | Prisma.WebsiteUncheckedUpdateInput, +): Promise { return prisma.client.website.update({ where: { id: websiteId, @@ -97,7 +100,9 @@ export async function deleteWebsite(websiteId: string) { }); } -async function deleteWebsiteRelationalQuery(websiteId,): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { +async function deleteWebsiteRelationalQuery( + websiteId, +): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; return transaction([ diff --git a/queries/index.js b/queries/index.js index 1a029edb..f1f03375 100644 --- a/queries/index.js +++ b/queries/index.js @@ -1,5 +1,3 @@ -export * from './admin/permission'; -export * from './admin/role'; export * from './admin/team'; export * from './admin/teamUser'; export * from './admin/user'; From 4ef48a8f4b9d9b1787f89984f9488c3603a72984 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Thu, 1 Dec 2022 22:06:36 -0800 Subject: [PATCH 29/38] Add 02 migration. --- .../migrations/02_update_role/migration.sql | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 db/postgresql/migrations/02_update_role/migration.sql diff --git a/db/postgresql/migrations/02_update_role/migration.sql b/db/postgresql/migrations/02_update_role/migration.sql new file mode 100644 index 00000000..3c957000 --- /dev/null +++ b/db/postgresql/migrations/02_update_role/migration.sql @@ -0,0 +1,74 @@ +/* + Warnings: + + - You are about to drop the column `role_id` on the `team_user` table. All the data in the column will be lost. + - You are about to drop the column `role_id` on the `user_role` table. All the data in the column will be lost. + - You are about to drop the `group` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `group_role` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `group_user` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `permission` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `role` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `role_permission` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[role,user_id]` on the table `user_role` will be added. If there are existing duplicate values, this will fail. + - Added the required column `role` to the `team_user` table without a default value. This is not possible if the table is not empty. + - Added the required column `role` to the `user_role` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "group_role" DROP CONSTRAINT "group_role_group_id_fkey"; + +-- DropForeignKey +ALTER TABLE "group_role" DROP CONSTRAINT "group_role_role_id_fkey"; + +-- DropForeignKey +ALTER TABLE "group_role" DROP CONSTRAINT "group_role_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "group_user" DROP CONSTRAINT "group_user_group_id_fkey"; + +-- DropForeignKey +ALTER TABLE "group_user" DROP CONSTRAINT "group_user_user_id_fkey"; + +-- DropForeignKey +ALTER TABLE "role_permission" DROP CONSTRAINT "role_permission_permission_id_fkey"; + +-- DropForeignKey +ALTER TABLE "role_permission" DROP CONSTRAINT "role_permission_role_id_fkey"; + +-- DropForeignKey +ALTER TABLE "team_user" DROP CONSTRAINT "team_user_role_id_fkey"; + +-- DropForeignKey +ALTER TABLE "user_role" DROP CONSTRAINT "user_role_role_id_fkey"; + +-- DropIndex +DROP INDEX "user_role_role_id_user_id_key"; + +-- AlterTable +ALTER TABLE "team_user" DROP COLUMN "role_id", +ADD COLUMN "role" VARCHAR(100) NOT NULL; + +-- AlterTable +ALTER TABLE "user_role" DROP COLUMN "role_id", +ADD COLUMN "role" VARCHAR(100) NOT NULL; + +-- DropTable +DROP TABLE "group"; + +-- DropTable +DROP TABLE "group_role"; + +-- DropTable +DROP TABLE "group_user"; + +-- DropTable +DROP TABLE "permission"; + +-- DropTable +DROP TABLE "role"; + +-- DropTable +DROP TABLE "role_permission"; + +-- CreateIndex +CREATE UNIQUE INDEX "user_role_role_user_id_key" ON "user_role"("role", "user_id"); From 4781a0cc8c60c4d6b5d37595fbc2b4484d0d025e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 2 Dec 2022 14:15:24 -0800 Subject: [PATCH 30/38] Added lib/types. --- lib/auth.ts | 21 +- lib/constants.ts | 60 ---- lib/types.ts | 57 ++++ package.json | 2 +- pages/api/users/[id]/role.ts | 4 +- queries/analytics/event/getEventData.ts | 6 +- queries/analytics/event/getEventMetrics.ts | 6 +- queries/analytics/event/saveEvent.ts | 6 +- .../analytics/pageview/getPageviewMetrics.ts | 6 +- .../analytics/pageview/getPageviewStats.ts | 6 +- queries/analytics/pageview/savePageView.ts | 6 +- yarn.lock | 309 +++++++++--------- 12 files changed, 247 insertions(+), 242 deletions(-) create mode 100644 lib/types.ts diff --git a/lib/auth.ts b/lib/auth.ts index abcb6c49..90515f85 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,9 +1,10 @@ +import { parseSecureToken, parseToken } from 'next-basics'; import { UserRole } from '@prisma/client'; import debug from 'debug'; import cache from 'lib/cache'; -import { SHARE_TOKEN_HEADER, UmamiApi } from 'lib/constants'; +import { SHARE_TOKEN_HEADER } from 'lib/constants'; import { secret } from 'lib/crypto'; -import { parseSecureToken, parseToken } from 'next-basics'; +import { Permission, Roles } from 'lib/types'; import { getTeamUser, getUserRoles } from 'queries'; const log = debug('umami:auth'); @@ -59,7 +60,7 @@ export async function canViewWebsite(userId: string, websiteId: string) { if (website.teamId) { const teamUser = await getTeamUser({ userId, teamId: website.teamId }); - checkPermission(UmamiApi.Permission.websiteUpdate, teamUser.role as keyof UmamiApi.Roles); + checkPermission(Permission.websiteUpdate, teamUser.role); } return checkAdmin(userId); @@ -75,7 +76,7 @@ export async function canUpdateWebsite(userId: string, websiteId: string) { if (website.teamId) { const teamUser = await getTeamUser({ userId, teamId: website.teamId }); - checkPermission(UmamiApi.Permission.websiteUpdate, teamUser.role as keyof UmamiApi.Roles); + checkPermission(Permission.websiteUpdate, teamUser.role); } return checkAdmin(userId); @@ -91,7 +92,7 @@ export async function canDeleteWebsite(userId: string, websiteId: string) { if (website.teamId) { const teamUser = await getTeamUser({ userId, teamId: website.teamId }); - if (checkPermission(UmamiApi.Permission.websiteDelete, teamUser.role as keyof UmamiApi.Roles)) { + if (checkPermission(Permission.websiteDelete, teamUser.role)) { return true; } } @@ -113,7 +114,7 @@ export async function canViewTeam(userId: string, teamId) { export async function canUpdateTeam(userId: string, teamId: string) { const teamUser = await getTeamUser({ userId, teamId }); - if (checkPermission(UmamiApi.Permission.teamUpdate, teamUser.role as keyof UmamiApi.Roles)) { + if (checkPermission(Permission.teamUpdate, teamUser.role)) { return true; } } @@ -121,7 +122,7 @@ export async function canUpdateTeam(userId: string, teamId: string) { export async function canDeleteTeam(userId: string, teamId: string) { const teamUser = await getTeamUser({ userId, teamId }); - if (checkPermission(UmamiApi.Permission.teamDelete, teamUser.role as keyof UmamiApi.Roles)) { + if (checkPermission(Permission.teamDelete, teamUser.role)) { return true; } } @@ -158,8 +159,8 @@ export async function canDeleteUser(userId: string) { return checkAdmin(userId); } -export async function checkPermission(permission: UmamiApi.Permission, role: keyof UmamiApi.Roles) { - return UmamiApi.Roles[role].permissions.some(a => a === permission); +export async function checkPermission(permission: Permission, role: string) { + return Roles[role].permissions.some(a => a === permission); } export async function checkAdmin(userId: string, userRoles?: UserRole[]) { @@ -167,5 +168,5 @@ export async function checkAdmin(userId: string, userRoles?: UserRole[]) { userRoles = await getUserRoles({ userId }); } - return userRoles.some(a => a.role === UmamiApi.Role.Admin); + return userRoles.some(a => a.role === Roles.admin.name); } diff --git a/lib/constants.ts b/lib/constants.ts index 0da17d21..e766791b 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,64 +1,4 @@ /* eslint-disable no-unused-vars */ -export namespace UmamiApi { - export enum EventType { - Pageview = 1, - Event = 2, - } - - export enum AuthType { - Website, - User, - Team, - } - - export enum Permission { - all = 'all', - websiteCreate = 'website:create', - websiteUpdate = 'website:update', - websiteDelete = 'website:delete', - teamCreate = 'team:create', - teamUpdate = 'team:update', - teamDelete = 'team:delete', - } - - export enum Role { - Admin = 'admin', - User = 'user', - TeamOwner = 'team-owner', - TeamMember = 'team-member', - TeamGuest = 'team-guest', - } - - export const Roles = { - admin: { name: Role.Admin, permissions: [Permission.all] }, - member: { - name: Role.User, - permissions: [ - Permission.websiteCreate, - Permission.websiteUpdate, - Permission.websiteDelete, - Permission.teamCreate, - ], - }, - teamOwner: { - name: Role.TeamOwner, - permissions: [ - Permission.teamUpdate, - Permission.teamDelete, - Permission.websiteCreate, - Permission.websiteUpdate, - Permission.websiteDelete, - ], - }, - teamMember: { - name: Role.TeamMember, - permissions: [Permission.websiteCreate, Permission.websiteUpdate, Permission.websiteDelete], - }, - teamGuest: { name: Role.TeamGuest, permissions: [] }, - }; - - export type Roles = typeof Roles; -} export const CURRENT_VERSION = process.env.currentVersion; export const AUTH_TOKEN = 'umami.auth'; export const LOCALE_CONFIG = 'umami.locale'; diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 00000000..425b5442 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,57 @@ +/* eslint-disable no-unused-vars */ +export enum EventType { + Pageview = 1, + Event = 2, +} + +export enum AuthType { + Website, + User, + Team, +} + +export enum Permission { + all = 'all', + websiteCreate = 'website:create', + websiteUpdate = 'website:update', + websiteDelete = 'website:delete', + teamCreate = 'team:create', + teamUpdate = 'team:update', + teamDelete = 'team:delete', +} + +export enum Role { + Admin = 'admin', + User = 'user', + TeamOwner = 'team-owner', + TeamMember = 'team-member', + TeamGuest = 'team-guest', +} + +export const Roles = { + admin: { name: Role.Admin, permissions: [Permission.all] }, + member: { + name: Role.User, + permissions: [ + Permission.websiteCreate, + Permission.websiteUpdate, + Permission.websiteDelete, + Permission.teamCreate, + ], + }, + teamOwner: { + name: Role.TeamOwner, + permissions: [ + Permission.teamUpdate, + Permission.teamDelete, + Permission.websiteCreate, + Permission.websiteUpdate, + Permission.websiteDelete, + ], + }, + teamMember: { + name: Role.TeamMember, + permissions: [Permission.websiteCreate, Permission.websiteUpdate, Permission.websiteDelete], + }, + teamGuest: { name: Role.TeamGuest, permissions: [] }, +}; diff --git a/package.json b/package.json index 5bc57774..331e58cf 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "npm-run-all": "^4.1.5", "prop-types": "^15.7.2", "react": "^18.2.0", - "react-basics": "^0.29.0", + "react-basics": "^0.33.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-intl": "^5.24.7", diff --git a/pages/api/users/[id]/role.ts b/pages/api/users/[id]/role.ts index fd4d33a6..61238077 100644 --- a/pages/api/users/[id]/role.ts +++ b/pages/api/users/[id]/role.ts @@ -1,7 +1,7 @@ import { UserRole } from '@prisma/client'; import { NextApiRequestQueryBody } from 'interface/api/nextApi'; import { canUpdateUserRole } from 'lib/auth'; -import { UmamiApi } from 'lib/constants'; +import { Role } from 'lib/types'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; @@ -11,7 +11,7 @@ export interface UserRoleRequestQuery { id: string; } export interface UserRoleRequestBody { - role: UmamiApi.Role; + role: Role; userRoleId?: string; } diff --git a/queries/analytics/event/getEventData.ts b/queries/analytics/event/getEventData.ts index 39002cf3..b6942685 100644 --- a/queries/analytics/event/getEventData.ts +++ b/queries/analytics/event/getEventData.ts @@ -3,7 +3,7 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; import { WebsiteMetric } from 'interface/api/models'; -import { UmamiApi } from 'lib/constants'; +import { EventType } from 'lib/types'; export async function getEventData( ...args: [ @@ -47,7 +47,7 @@ async function relationalQuery( from website_event where website_id ='${websiteId}' and created_at between $1 and $2 - and event_type = ${UmamiApi.EventType.Event} + and event_type = ${EventType.Event} ${eventName ? `and eventName = ${eventName}` : ''} ${ Object.keys(filters).length > 0 @@ -80,7 +80,7 @@ async function clickhouseQuery( from event where website_id = $1 and rev_id = $2 - and event_type = ${UmamiApi.EventType.Event} + and event_type = ${EventType.Event} ${eventName ? `and eventName = ${eventName}` : ''} and ${getBetweenDates('created_at', startDate, endDate)} ${ diff --git a/queries/analytics/event/getEventMetrics.ts b/queries/analytics/event/getEventMetrics.ts index ebf855f0..a7557091 100644 --- a/queries/analytics/event/getEventMetrics.ts +++ b/queries/analytics/event/getEventMetrics.ts @@ -3,7 +3,7 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; import { WebsiteEventMetric } from 'interface/api/models'; -import { UmamiApi } from 'lib/constants'; +import { EventType } from 'lib/types'; export async function getEventMetrics( ...args: [ @@ -56,7 +56,7 @@ async function relationalQuery( from website_event where website_id='${websiteId}' and created_at between $1 and $2 - and event_type = ${UmamiApi.EventType.Event} + and event_type = ${EventType.Event} ${getFilterQuery(filters, params)} group by 1, 2 order by 2`, @@ -95,7 +95,7 @@ async function clickhouseQuery( from event where website_id = $1 and rev_id = $2 - and event_type = ${UmamiApi.EventType.Event} + and event_type = ${EventType.Event} and ${getBetweenDates('created_at', startDate, endDate)} ${getFilterQuery(filters, params)} group by x, t diff --git a/queries/analytics/event/saveEvent.ts b/queries/analytics/event/saveEvent.ts index c2a92d10..3cfa8902 100644 --- a/queries/analytics/event/saveEvent.ts +++ b/queries/analytics/event/saveEvent.ts @@ -4,7 +4,7 @@ import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import { uuid } from 'lib/crypto'; import cache from 'lib/cache'; -import { UmamiApi } from 'lib/constants'; +import { EventType } from 'lib/types'; export async function saveEvent(args: { id: string; @@ -43,7 +43,7 @@ async function relationalQuery(data: { sessionId, url: url?.substring(0, URL_LENGTH), referrer: referrer?.substring(0, URL_LENGTH), - eventType: UmamiApi.EventType.Event, + eventType: EventType.Event, eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventData, }; @@ -77,7 +77,7 @@ async function clickhouseQuery(data: { session_id: sessionId, event_id: uuid(), url: url?.substring(0, URL_LENGTH), - event_type: UmamiApi.EventType.Event, + event_type: EventType.Event, event_name: eventName?.substring(0, EVENT_NAME_LENGTH), event_data: eventData ? JSON.stringify(eventData) : null, rev_id: website?.revId || 0, diff --git a/queries/analytics/pageview/getPageviewMetrics.ts b/queries/analytics/pageview/getPageviewMetrics.ts index f61e25bd..402f6eb1 100644 --- a/queries/analytics/pageview/getPageviewMetrics.ts +++ b/queries/analytics/pageview/getPageviewMetrics.ts @@ -3,7 +3,7 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import cache from 'lib/cache'; import { Prisma } from '@prisma/client'; -import { UmamiApi } from 'lib/constants'; +import { EventType } from 'lib/types'; export async function getPageviewMetrics( ...args: [ @@ -43,7 +43,7 @@ async function relationalQuery( ${joinSession} where website_id='${websiteId}' and website_event.created_at between $1 and $2 - and event_type = ${UmamiApi.EventType.Pageview} + and event_type = ${EventType.Pageview} ${filterQuery} group by 1 order by 2 desc`, @@ -71,7 +71,7 @@ async function clickhouseQuery( from event where website_id = $1 and rev_id = $2 - and event_type = ${UmamiApi.EventType.Pageview} + and event_type = ${EventType.Pageview} ${column !== 'event_name' ? `and event_name = ''` : `and event_name != ''`} and ${getBetweenDates('created_at', startDate, endDate)} ${filterQuery} diff --git a/queries/analytics/pageview/getPageviewStats.ts b/queries/analytics/pageview/getPageviewStats.ts index 212a07de..ab23cb15 100644 --- a/queries/analytics/pageview/getPageviewStats.ts +++ b/queries/analytics/pageview/getPageviewStats.ts @@ -2,7 +2,7 @@ import cache from 'lib/cache'; import clickhouse from 'lib/clickhouse'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import prisma from 'lib/prisma'; -import { UmamiApi } from 'lib/constants'; +import { EventType } from 'lib/types'; export async function getPageviewStats( ...args: [ @@ -56,7 +56,7 @@ async function relationalQuery( ${joinSession} where website.website_id='${websiteId}' and pageview.created_at between $1 and $2 - and event_type = ${UmamiApi.EventType.Pageview} + and event_type = ${EventType.Pageview} ${filterQuery} group by 1`, params, @@ -92,7 +92,7 @@ async function clickhouseQuery( from event where website_id = $1 and rev_id = $2 - and event_type = ${UmamiApi.EventType.Pageview} + and event_type = ${EventType.Pageview} and ${getBetweenDates('created_at', startDate, endDate)} ${filterQuery} group by t) g diff --git a/queries/analytics/pageview/savePageView.ts b/queries/analytics/pageview/savePageView.ts index b73b8c0a..7e8c3b37 100644 --- a/queries/analytics/pageview/savePageView.ts +++ b/queries/analytics/pageview/savePageView.ts @@ -4,7 +4,7 @@ import kafka from 'lib/kafka'; import prisma from 'lib/prisma'; import cache from 'lib/cache'; import { uuid } from 'lib/crypto'; -import { UmamiApi } from 'lib/constants'; +import { EventType } from 'lib/types'; export async function savePageView(args: { id: string; @@ -40,7 +40,7 @@ async function relationalQuery(data: { sessionId, url: url?.substring(0, URL_LENGTH), referrer: referrer?.substring(0, URL_LENGTH), - eventType: UmamiApi.EventType.Pageview, + eventType: EventType.Pageview, }, }); } @@ -58,7 +58,7 @@ async function clickhouseQuery(data) { rev_id: website?.revId || 0, created_at: getDateFormat(new Date()), country: country ? country : null, - event_type: UmamiApi.EventType.Pageview, + event_type: EventType.Pageview, ...args, }; diff --git a/yarn.lock b/yarn.lock index d3f9f408..bb118a91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1050,20 +1050,27 @@ "@babel/plugin-transform-typescript" "^7.18.6" "@babel/runtime-corejs3@^7.10.2": - version "7.20.1" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.20.1.tgz#d0775a49bb5fba77e42cbb7276c9955c7b05af8d" - integrity sha512-CGulbEDcg/ND1Im7fUNRZdGXmX2MTWVVZacQi/6DiKE5HNwZ3aVTm5PV4lO8HHz0B2h8WQyvKKjbX5XgTtydsg== + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz#63dae945963539ab0ad578efbf3eff271e7067ae" + integrity sha512-tqeujPiuEfcH067mx+7otTQWROVMKHXEaOQcAeNV5dDdbPWvPcFA8/W9LXw2NfjNmOetqLl03dfnG2WALPlsRQ== dependencies: core-js-pure "^3.25.1" - regenerator-runtime "^0.13.10" + regenerator-runtime "^0.13.11" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.9", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== dependencies: regenerator-runtime "^0.13.10" +"@babel/runtime@^7.10.2", "@babel/runtime@^7.18.9": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.8.4": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.0.tgz#824a9ef325ffde6f78056059db3168c08785e24a" @@ -1606,82 +1613,82 @@ slash "^3.0.0" tiny-glob "^0.2.9" -"@next/env@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.3.tgz#26c8e8f0f65da382d1a537cb8df30d63fc5d828a" - integrity sha512-H2pKuOasV9RgvVaWosB2rGSNeQShQpiDaF4EEjLyagIc3HwqdOw2/VAG/8Lq+adOwPv2P73O1hulTNad3k5MDw== +"@next/env@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.4.tgz#c787837d36fcad75d72ff8df6b57482027d64a47" + integrity sha512-H/69Lc5Q02dq3o+dxxy5O/oNxFsZpdL6WREtOOtOM1B/weonIwDXkekr1KV5DPVPr12IHFPrMrcJQ6bgPMfn7A== -"@next/eslint-plugin-next@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.3.tgz#4c2eb595101a1778d5ff7c78574f7e810f72b5e5" - integrity sha512-s1mPMhhmwc+B97lQ2xzLLEdn3TR6ietc8Z1zLhAEd5Vujqx+Ks7E8Qr8V93I/qTs21WY66zvs1SXKYLvOHbQVw== +"@next/eslint-plugin-next@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.3.4.tgz#e7dc00e2e89ed361f111d687b8534483ec15518b" + integrity sha512-BFwj8ykJY+zc1/jWANsDprDIu2MgwPOIKxNVnrKvPs+f5TPegrVnem8uScND+1veT4B7F6VeqgaNLFW1Hzl9Og== dependencies: glob "7.1.7" -"@next/swc-android-arm-eabi@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.3.tgz#1173a8e9ddb92c9d2d1a4fc29c5397f3d815c1ef" - integrity sha512-5O/ZIX6hlIRGMy1R2f/8WiCZ4Hp4WTC0FcTuz8ycQ28j/mzDnmzjVoayVVr+ZmfEKQayFrRu+vxHjFyY0JGQlQ== +"@next/swc-android-arm-eabi@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz#fd1c2dafe92066c6120761c6a39d19e666dc5dd0" + integrity sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA== -"@next/swc-android-arm64@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.3.tgz#8e49a1486ff1c5e6f4760ad31a2fa3cfcc5a3329" - integrity sha512-2QWreRmlxYRDtnLYn+BI8oukHwcP7W0zGIY5R2mEXRjI4ARqCLdu8RmcT9Vemw7RfeAVKA/4cv/9PY0pCcQpNA== +"@next/swc-android-arm64@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz#11a146dae7b8bca007239b21c616e83f77b19ed4" + integrity sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg== -"@next/swc-darwin-arm64@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.3.tgz#92618ffea1d0d128db0787854afe368b5976837a" - integrity sha512-GtZdDLerM+VToCMFp+W+WhnT6sxHePQH4xZZiYD/Y8KFiwHbDRcJr2FPG0bAJnGNiSvv/QQnBq74wjZ9+7vhcQ== +"@next/swc-darwin-arm64@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz#14ac8357010c95e67327f47082af9c9d75d5be79" + integrity sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA== -"@next/swc-darwin-x64@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.3.tgz#76a5a496cc7ead3cc02aaca84d1ed02dff86e029" - integrity sha512-gRYvTKrRYynjFQUDJ+upHMcBiNz0ii0m7zGgmUTlTSmrBWqVSzx79EHYT7Nn4GWHM+a/W+2VXfu+lqHcJeQ9gQ== +"@next/swc-darwin-x64@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz#e7dc63cd2ac26d15fb84d4d2997207fb9ba7da0f" + integrity sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ== -"@next/swc-freebsd-x64@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.3.tgz#d546fb7060adf0cd27c6a8c1abca5c58f62c8f06" - integrity sha512-r+GLATzCjjQI82bgrIPXWEYBwZonSO64OThk5wU6HduZlDYTEDxZsFNoNoesCDWCgRrgg+OXj7WLNy1WlvfX7w== +"@next/swc-freebsd-x64@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz#fe7ceec58746fdf03f1fcb37ec1331c28e76af93" + integrity sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ== -"@next/swc-linux-arm-gnueabihf@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.3.tgz#525f451e6e1d134e064707c5c761b6d5d6bb3c7e" - integrity sha512-juvRj1QX9jmQScL4nV0rROtYUFgWP76zfdn1fdfZ2BhvwUugIAq8x+jLVGlnXKUhDrP9+RrAufqXjjVkK+uBxA== +"@next/swc-linux-arm-gnueabihf@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz#d7016934d02bfc8bd69818ffb0ae364b77b17af7" + integrity sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw== -"@next/swc-linux-arm64-gnu@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.3.tgz#13aa5dfeef0de52eac1220ab22cabdee6447bb3a" - integrity sha512-hzinybStPB+SzS68hR5rzOngOH7Yd/jFuWGeg9qS5WifYXHpqwGH2BQeKpjVV0iJuyO9r309JKrRWMrbfhnuBA== +"@next/swc-linux-arm64-gnu@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz#43a7bc409b03487bff5beb99479cacdc7bd29af5" + integrity sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA== -"@next/swc-linux-arm64-musl@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.3.tgz#4bdae82882c1e31a1008f20f544b1954a21d385f" - integrity sha512-oyfQYljCwf+9zUu1YkTZbRbyxmcHzvJPMGOxC3kJOReh3kCUoGcmvAxUPMtFD6FSYjJ+eaog4+2IFHtYuAw/bQ== +"@next/swc-linux-arm64-musl@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz#4d1db6de6dc982b974cd1c52937111e3e4a34bd3" + integrity sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ== -"@next/swc-linux-x64-gnu@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.3.tgz#0697b1fc60dc4a86a7260f60e983d9064a331b2c" - integrity sha512-epv4FMazj/XG70KTTnrZ0H1VtL6DeWOvyHLHYy7f5PdgDpBXpDTFjVqhP8NFNH8HmaDDdeL1NvQD07AXhyvUKA== +"@next/swc-linux-x64-gnu@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz#c3b414d77bab08b35f7dd8943d5586f0adb15e38" + integrity sha512-4csPbRbfZbuWOk3ATyWcvVFdD9/Rsdq5YHKvRuEni68OCLkfy4f+4I9OBpyK1SKJ00Cih16NJbHE+k+ljPPpag== -"@next/swc-linux-x64-musl@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.3.tgz#af012e65035fcd7cc3855ce90b4095d2c7b879a5" - integrity sha512-bG5QODFy59XnSFTiPyIAt+rbPdphtvQMibtOVvyjwIwsBUw7swJ6k+6PSPVYEYpi6SHzi3qYBsro39ayGJKQJg== +"@next/swc-linux-x64-musl@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.4.tgz#187a883ec09eb2442a5ebf126826e19037313c61" + integrity sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg== -"@next/swc-win32-arm64-msvc@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.3.tgz#bf717c96ce17f63840e3cbb023725cb3872ed0b3" - integrity sha512-FbnT3reJ3MbTJ5W0hvlCCGGVDSpburzT5XGC9ljBJ4kr+85iNTLjv7+vrPeDdwHEqtGmdZgnabkLVCI4yFyCag== +"@next/swc-win32-arm64-msvc@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz#89befa84e453ed2ef9a888f375eba565a0fde80b" + integrity sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ== -"@next/swc-win32-ia32-msvc@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.3.tgz#f434bc4bd952af77070868b5fa32ced85b52a646" - integrity sha512-M/fKZC2tMGWA6eTsIniNEBpx2prdR8lIxvSO3gv5P6ymZOGVWCvEMksnTkPAjHnU6d8r8eCiuGKm3UNo7zCTpQ== +"@next/swc-win32-ia32-msvc@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz#cb50c08f0e40ead63642a7f269f0c8254261f17c" + integrity sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ== -"@next/swc-win32-x64-msvc@12.3.3": - version "12.3.3" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.3.tgz#1b412e9e15550680e1fdba70daa8b6ddcc75a035" - integrity sha512-Ku9mfGwmNtk44o4B/jEWUxBAT4tJ3S7QbBMLJdL1GmtRZ05LGL36OqWjLvBPr8dFiHOQQbYoAmYfQw7zeGypYA== +"@next/swc-win32-x64-msvc@12.3.4": + version "12.3.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz#d28ea15a72cdcf96201c60a43e9630cd7fda168f" + integrity sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1812,10 +1819,10 @@ resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.1.0.tgz#64e310ddee72010676e14296076329e594a1f6c7" integrity sha512-9QovlxmpRtvxVbN0UBcv8WfdSMudNZZTFqCsnBszcQXqaZb/TVe30ScgGEO7u1EAIacTPAo7/oCYjYAxiHLanQ== -"@redis/client@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.4.0.tgz#d2c56ce26c3e2fe3412db5cfb1814169662167eb" - integrity sha512-1gEj1AkyXPlkcC/9/T5xpDcQF8ntERURjLBgEWMTdUZqe181zfI9BY3jc2OzjTLkvZh5GV7VT4ktoJG2fV2ufw== +"@redis/client@1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.4.2.tgz#2a3f5e98bc33b7b979390442e6e08f96e57fabdd" + integrity sha512-oUdEjE0I7JS5AyaAjkD3aOXn9NhO7XKyPyXEyrgFDu++VrVBHUPnV6dgEya9TcMuj5nIJRuCzCm8ZP+c9zCHPw== dependencies: cluster-key-slot "1.1.1" generic-pool "3.9.0" @@ -1985,17 +1992,17 @@ dependencies: tslib "^2.4.0" -"@tanstack/query-core@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.15.1.tgz#a282f04fe5e612b50019e1cfaf0efabd220e00ce" - integrity sha512-+UfqJsNbPIVo0a9ANW0ZxtjiMfGLaaoIaL9vZeVycvmBuWywJGtSi7fgPVMCPdZQFOzMsaXaOsDtSKQD5xLRVQ== +"@tanstack/query-core@4.19.0": + version "4.19.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.19.0.tgz#be1ad7bfbc4edb68e21ab2a86dd4c52233a40c87" + integrity sha512-q+4GvS05nG2UXDE4ng0NU5SQNhT+VqhRTLNVtgVw1tIKJfG3CyYQpP/JwAdzMB7NEqC8L5oo9NAaORxEQN53dg== "@tanstack/react-query@^4.16.1": - version "4.16.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.16.1.tgz#077006b8eb2c87fbe8d1597c1a0083a2d218b791" - integrity sha512-PDE9u49wSDykPazlCoLFevUpceLjQ0Mm8i6038HgtTEKb/aoVnUZdlUP7C392ds3Cd75+EGlHU7qpEX06R7d9Q== + version "4.19.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.19.0.tgz#353169522ff2e6dcfcc36c95ba4c151974a7ec92" + integrity sha512-gP4kmfQ3BvCYxTxA/3Xf0P24iNgW539Thk89KzP7X+i+EvFiWhEUMl1NtuI87bFrVEBHs+1ColFNimDidBh/Ww== dependencies: - "@tanstack/query-core" "4.15.1" + "@tanstack/query-core" "4.19.0" use-sync-external-store "^1.2.0" "@trysound/sax@0.2.0": @@ -2154,47 +2161,47 @@ schema-utils "*" "@typescript-eslint/parser@^5.21.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.44.0.tgz#99e2c710a2252191e7a79113264f438338b846ad" - integrity sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA== + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e" + integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ== dependencies: - "@typescript-eslint/scope-manager" "5.44.0" - "@typescript-eslint/types" "5.44.0" - "@typescript-eslint/typescript-estree" "5.44.0" + "@typescript-eslint/scope-manager" "5.45.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/typescript-estree" "5.45.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.44.0.tgz#988c3f34b45b3474eb9ff0674c18309dedfc3e04" - integrity sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g== +"@typescript-eslint/scope-manager@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96" + integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw== dependencies: - "@typescript-eslint/types" "5.44.0" - "@typescript-eslint/visitor-keys" "5.44.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/visitor-keys" "5.45.0" -"@typescript-eslint/types@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.44.0.tgz#f3f0b89aaff78f097a2927fe5688c07e786a0241" - integrity sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ== +"@typescript-eslint/types@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5" + integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA== -"@typescript-eslint/typescript-estree@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.44.0.tgz#0461b386203e8d383bb1268b1ed1da9bc905b045" - integrity sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw== +"@typescript-eslint/typescript-estree@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" + integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ== dependencies: - "@typescript-eslint/types" "5.44.0" - "@typescript-eslint/visitor-keys" "5.44.0" + "@typescript-eslint/types" "5.45.0" + "@typescript-eslint/visitor-keys" "5.45.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@5.44.0": - version "5.44.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.44.0.tgz#10740dc28902bb903d12ee3a005cc3a70207d433" - integrity sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ== +"@typescript-eslint/visitor-keys@5.45.0": + version "5.45.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528" + integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg== dependencies: - "@typescript-eslint/types" "5.44.0" + "@typescript-eslint/types" "5.45.0" eslint-visitor-keys "^3.3.0" "@vercel/node-bridge@^2.1.0": @@ -2770,9 +2777,9 @@ caniuse-lite@^1.0.30001400: integrity sha512-lfXQ73oB9c8DP5Suxaszm+Ta2sr/4tf8+381GkIm1MLj/YdLf+rEDyDSRCzeltuyTVGm+/s18gdZ0q+Wmp8VsQ== caniuse-lite@^1.0.30001406: - version "1.0.30001434" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz#ec1ec1cfb0a93a34a0600d37903853030520a4e5" - integrity sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA== + version "1.0.30001435" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001435.tgz#502c93dbd2f493bee73a408fe98e98fb1dad10b2" + integrity sha512-kdCkUTjR+v4YAJelyiDTqiu82BDr4W4CP5sgTA0ZBmqn30XfS2ZghPLMowik9TPhS+psWJiUNxsqLyurDbmutA== caseless@~0.12.0: version "0.12.0" @@ -3588,11 +3595,11 @@ escape-string-regexp@^4.0.0: integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-config-next@^12.2.4: - version "12.3.3" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.3.3.tgz#b04f6b55b43a72983e68e51329a993d2a8ea648c" - integrity sha512-ZqovaLqMlWQh9yVbqJ2gvOLk6acAZX4vRkORFsiI5lv9oJDDBbDDeTPG2KmpZ3K+l/wJ+xo6bm4FN90j94snhw== + version "12.3.4" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-12.3.4.tgz#3d4d9e74b919b879c4cc79c61bdc388fb2b964ee" + integrity sha512-WuT3gvgi7Bwz00AOmKGhOeqnyA5P29Cdyr0iVjLyfDbk+FANQKcOjFUTZIdyYfe5Tq1x4TGcmoe4CwctGvFjHQ== dependencies: - "@next/eslint-plugin-next" "12.3.3" + "@next/eslint-plugin-next" "12.3.4" "@rushstack/eslint-patch" "^1.1.3" "@typescript-eslint/parser" "^5.21.0" eslint-import-resolver-node "^0.3.6" @@ -4409,9 +4416,9 @@ ignore@^4.0.6: integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + version "5.2.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== image-meta@^0.1.1: version "0.1.1" @@ -4948,17 +4955,17 @@ known-css-properties@^0.26.0: resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.26.0.tgz#008295115abddc045a9f4ed7e2a84dc8b3a77649" integrity sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg== -language-subtag-registry@~0.3.2: +language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== language-tags@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== + version "1.0.6" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.6.tgz#c087cc42cd92eb71f0925e9e271d4f8be5a93430" + integrity sha512-HNkaCgM8wZgE/BZACeotAAgpL9FUjEnhgF0FVQMIgH//zqTPreLYMb3rWYkYAqPoF75Jwuycp1da7uz66cfFQg== dependencies: - language-subtag-registry "~0.3.2" + language-subtag-registry "^0.3.20" levn@^0.4.1: version "0.4.1" @@ -5448,30 +5455,30 @@ next-basics@^0.23.0: jsonwebtoken "^8.5.1" next@^12.3.1: - version "12.3.3" - resolved "https://registry.yarnpkg.com/next/-/next-12.3.3.tgz#c1286fc24e378b0b0279ef205db7d8dd994dcd79" - integrity sha512-Rx2Y6Wl5R8E77NOfBupp/B9OPCklqfqD0yN2+rDivhMjd6hjVFH5n0WTDI4PWwDmZsdNcYt6NV85kJ3PLR+eNQ== + version "12.3.4" + resolved "https://registry.yarnpkg.com/next/-/next-12.3.4.tgz#f2780a6ebbf367e071ce67e24bd8a6e05de2fcb1" + integrity sha512-VcyMJUtLZBGzLKo3oMxrEF0stxh8HwuW976pAzlHhI3t8qJ4SROjCrSh1T24bhrbjw55wfZXAbXPGwPt5FLRfQ== dependencies: - "@next/env" "12.3.3" + "@next/env" "12.3.4" "@swc/helpers" "0.4.11" caniuse-lite "^1.0.30001406" postcss "8.4.14" styled-jsx "5.0.7" use-sync-external-store "1.2.0" optionalDependencies: - "@next/swc-android-arm-eabi" "12.3.3" - "@next/swc-android-arm64" "12.3.3" - "@next/swc-darwin-arm64" "12.3.3" - "@next/swc-darwin-x64" "12.3.3" - "@next/swc-freebsd-x64" "12.3.3" - "@next/swc-linux-arm-gnueabihf" "12.3.3" - "@next/swc-linux-arm64-gnu" "12.3.3" - "@next/swc-linux-arm64-musl" "12.3.3" - "@next/swc-linux-x64-gnu" "12.3.3" - "@next/swc-linux-x64-musl" "12.3.3" - "@next/swc-win32-arm64-msvc" "12.3.3" - "@next/swc-win32-ia32-msvc" "12.3.3" - "@next/swc-win32-x64-msvc" "12.3.3" + "@next/swc-android-arm-eabi" "12.3.4" + "@next/swc-android-arm64" "12.3.4" + "@next/swc-darwin-arm64" "12.3.4" + "@next/swc-darwin-x64" "12.3.4" + "@next/swc-freebsd-x64" "12.3.4" + "@next/swc-linux-arm-gnueabihf" "12.3.4" + "@next/swc-linux-arm64-gnu" "12.3.4" + "@next/swc-linux-arm64-musl" "12.3.4" + "@next/swc-linux-x64-gnu" "12.3.4" + "@next/swc-linux-x64-musl" "12.3.4" + "@next/swc-win32-arm64-msvc" "12.3.4" + "@next/swc-win32-ia32-msvc" "12.3.4" + "@next/swc-win32-x64-msvc" "12.3.4" nice-try@^1.0.4: version "1.0.5" @@ -6193,9 +6200,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^2.6.2: - version "2.7.1" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz" - integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + version "2.8.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9" + integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA== pretty-bytes@^5.6.0: version "5.6.0" @@ -6301,10 +6308,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.29.0: - version "0.29.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.29.0.tgz#f63deb74a9ddb4097752f0d7e1283fcaa69c35fe" - integrity sha512-cj3dlzDNZc3XUeqgwQl4QQ/5lEpAxgewHBg63F4jjY2Jpph4WoKdYj6bd1KFEGWifbDFFNZSLE39UWwC9gh+gg== +react-basics@^0.33.0: + version "0.33.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.33.0.tgz#f542ca76efd4f2d1df67f871159292d5d4562515" + integrity sha512-lYr98TFn5FPEKKfXrP/t1noWZMVo4cMW1ngdjYmN5vCIO1FAHe40/f+XXKx3yh3BFZhgS/iP/mU8f+q1b6uEpw== dependencies: classnames "^2.3.1" react "^18.2.0" @@ -6405,9 +6412,9 @@ react-spring@^9.4.4, react-spring@^9.5.5: "@react-spring/zdog" "~9.5.5" react-tooltip@^4.2.21: - version "4.5.0" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.5.0.tgz#862a39fbb05522624fb6efa782b245a89a0db784" - integrity sha512-mJNurq29atce+TJc9Xe+/FHrcEs3K9J7wkjZZXwbK5Yq6uG5SZeKSFHwd0wcRPUipVwx5crmgzSW8Zu1xyvLTQ== + version "4.5.1" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.5.1.tgz#77eccccdf16adec804132e558ec20ca5783b866a" + integrity sha512-Zo+CSFUGXar1uV+bgXFFDe7VeS2iByeIp5rTgTcc2HqtuOS5D76QapejNNfx320MCY91TlhTQat36KGFTqgcvw== dependencies: prop-types "^15.8.1" uuid "^7.0.3" @@ -6514,12 +6521,12 @@ redis-parser@^3.0.0: redis-errors "^1.0.0" redis@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/redis/-/redis-4.5.0.tgz#8a461c8718e380ea899ba3711aa0bb217b112089" - integrity sha512-oZGAmOKG+RPnHo0UxM5GGjJ0dBd/Vi4fs3MYwM1p2baDoXC0wpm0yOdpxVS9K+0hM84ycdysp2eHg2xGoQ4FEw== + version "4.5.1" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.5.1.tgz#f5a818970bb2dc5d60540bab41308640604c7d33" + integrity sha512-oxXSoIqMJCQVBTfxP6BNTCtDMyh9G6Vi5wjdPdV/sRKkufyZslDqCScSGcOr6XGR/reAWZefz7E4leM31RgdBA== dependencies: "@redis/bloom" "1.1.0" - "@redis/client" "1.4.0" + "@redis/client" "1.4.2" "@redis/graph" "1.1.0" "@redis/json" "1.0.4" "@redis/search" "1.1.0" @@ -6551,7 +6558,7 @@ regenerate@^1.4.0, regenerate@^1.4.2: resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.10: +regenerator-runtime@^0.13.10, regenerator-runtime@^0.13.11: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== From 4eb3140e43bcc5f96289f0a964adbfcd1a23b6eb Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 5 Dec 2022 13:03:02 -0800 Subject: [PATCH 31/38] Updated schema. --- db/postgresql/schema.prisma | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index 7ef3dd88..df273edf 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -11,10 +11,11 @@ model User { id String @id @unique @map("user_id") @db.Uuid username String @unique @db.VarChar(255) password String @db.VarChar(60) + role String @map("role") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") + updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) - userRole UserRole[] teamUser TeamUser[] Website Website[] @@ -47,7 +48,8 @@ model Website { userId String? @map("user_id") @db.Uuid teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") + updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) team Team? @relation(fields: [teamId], references: [id]) user User? @relation(fields: [userId], references: [id]) @@ -76,24 +78,13 @@ model WebsiteEvent { @@map("website_event") } -model UserRole { - id String @id() @unique() @map("user_role_id") @db.Uuid - role String @map("role") @db.VarChar(100) - userId String @map("user_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") - - user User @relation(fields: [userId], references: [id]) - - @@unique([role, userId]) - @@map("user_role") -} - model Team { id String @id() @unique() @map("team_id") @db.Uuid - name String @unique() @db.VarChar(50) + name String @db.VarChar(50) + userId String @map("user_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") + updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) teamUsers TeamUser[] Website Website[] @@ -105,9 +96,10 @@ model TeamUser { id String @id() @unique() @map("team_user_id") @db.Uuid teamId String @map("team_id") @db.Uuid userId String @map("user_id") @db.Uuid - role String @map("role") @db.VarChar(100) + role String @map("role") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - isDeleted Boolean @default(false) @map("is_deleted") + updatedAt DateTime? @map("updated_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) team Team @relation(fields: [teamId], references: [id]) user User @relation(fields: [userId], references: [id]) From b57ecf33e663fe7732e42babb6c4dc2ce031a5bc Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 6 Dec 2022 18:36:41 -0800 Subject: [PATCH 32/38] Updated roles and permissions logic. --- assets/arrow-right.svg | 2 +- assets/arrow-up-right-from-square.svg | 2 +- assets/bars.svg | 2 +- assets/chart-bar.svg | 2 +- assets/check.svg | 2 +- assets/chevron-down.svg | 2 +- assets/code.svg | 2 +- assets/external-link.svg | 2 +- assets/gear.svg | 2 +- assets/logo.svg | 3 +- assets/moon.svg | 2 +- assets/pen.svg | 2 +- assets/sun.svg | 2 +- assets/times.svg | 2 +- assets/user.svg | 2 +- assets/xmark.svg | 2 +- components/layout/Layout.js | 4 +- components/pages/WebsiteDetails.js | 4 +- interface/api/auth.d.ts | 8 -- interface/api/models.d.ts | 66 --------- interface/api/nextApi.d.ts | 14 -- interface/index.d.ts | 0 lib/auth.ts | 83 +++-------- lib/constants.ts | 46 ++++++ lib/middleware.js | 5 + lib/redis.js | 2 - lib/session.js | 2 +- lib/types.ts | 131 +++++++++++------- pages/api/auth/login.ts | 2 +- pages/api/auth/verify.ts | 2 +- pages/api/realtime/init.ts | 4 +- pages/api/realtime/update.ts | 4 +- pages/api/share/[id].ts | 2 +- pages/api/teams/[id]/index.ts | 8 +- pages/api/teams/[id]/{user.ts => users.ts} | 12 +- .../teams/[id]/{website.ts => websites.ts} | 12 +- pages/api/teams/index.ts | 19 +-- pages/api/users/[id]/index.ts | 10 +- pages/api/users/[id]/password.ts | 2 +- pages/api/users/[id]/role.ts | 62 --------- pages/api/users/[id]/websites.ts | 57 ++++++++ pages/api/users/index.ts | 15 +- pages/api/websites/[id]/active.ts | 4 +- pages/api/websites/[id]/eventdata.ts | 4 +- pages/api/websites/[id]/events.ts | 4 +- pages/api/websites/[id]/index.ts | 4 +- pages/api/websites/[id]/metrics.ts | 4 +- pages/api/websites/[id]/pageviews.ts | 3 +- pages/api/websites/[id]/reset.ts | 2 +- pages/api/websites/[id]/stats.ts | 4 +- pages/api/websites/index.ts | 16 +-- queries/admin/team.ts | 57 +++++--- queries/admin/teamUser.ts | 31 +++-- queries/admin/user.ts | 86 +++++++----- queries/admin/userRole.ts | 51 ------- queries/admin/website.ts | 54 ++------ queries/analytics/event/getEventData.ts | 8 +- queries/analytics/event/getEventMetrics.ts | 8 +- queries/analytics/event/saveEvent.ts | 7 +- .../analytics/pageview/getPageviewMetrics.ts | 8 +- .../analytics/pageview/getPageviewStats.ts | 6 +- queries/analytics/pageview/savePageView.ts | 7 +- queries/index.js | 1 - 63 files changed, 432 insertions(+), 546 deletions(-) delete mode 100644 interface/api/auth.d.ts delete mode 100644 interface/api/models.d.ts delete mode 100644 interface/api/nextApi.d.ts delete mode 100644 interface/index.d.ts rename pages/api/teams/[id]/{user.ts => users.ts} (81%) rename pages/api/teams/[id]/{website.ts => websites.ts} (78%) delete mode 100644 pages/api/users/[id]/role.ts create mode 100644 pages/api/users/[id]/websites.ts delete mode 100644 queries/admin/userRole.ts diff --git a/assets/arrow-right.svg b/assets/arrow-right.svg index 6fc93909..efc5d74a 100644 --- a/assets/arrow-right.svg +++ b/assets/arrow-right.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/arrow-up-right-from-square.svg b/assets/arrow-up-right-from-square.svg index 90ad457f..8f6de672 100644 --- a/assets/arrow-up-right-from-square.svg +++ b/assets/arrow-up-right-from-square.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/bars.svg b/assets/bars.svg index fdb2d6e4..ba383fa4 100644 --- a/assets/bars.svg +++ b/assets/bars.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/chart-bar.svg b/assets/chart-bar.svg index d1d72fdc..36820b76 100644 --- a/assets/chart-bar.svg +++ b/assets/chart-bar.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/check.svg b/assets/check.svg index 1a7abdce..65810c19 100644 --- a/assets/check.svg +++ b/assets/check.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/chevron-down.svg b/assets/chevron-down.svg index cb9d8fe1..69add632 100644 --- a/assets/chevron-down.svg +++ b/assets/chevron-down.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/code.svg b/assets/code.svg index cd29765e..0f8e0814 100644 --- a/assets/code.svg +++ b/assets/code.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/external-link.svg b/assets/external-link.svg index ed09306f..e24896b0 100644 --- a/assets/external-link.svg +++ b/assets/external-link.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/gear.svg b/assets/gear.svg index ab97a693..47805d46 100644 --- a/assets/gear.svg +++ b/assets/gear.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/logo.svg b/assets/logo.svg index f0e52261..d2c71326 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/assets/moon.svg b/assets/moon.svg index 6c8955ae..638286fd 100644 --- a/assets/moon.svg +++ b/assets/moon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/pen.svg b/assets/pen.svg index 426c520c..b2979420 100644 --- a/assets/pen.svg +++ b/assets/pen.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/sun.svg b/assets/sun.svg index ebc20eb2..3e487291 100644 --- a/assets/sun.svg +++ b/assets/sun.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/times.svg b/assets/times.svg index c528bcdd..261bb277 100644 --- a/assets/times.svg +++ b/assets/times.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/user.svg b/assets/user.svg index c0094666..62df2c42 100644 --- a/assets/user.svg +++ b/assets/user.svg @@ -1 +1 @@ -Asset 1 \ No newline at end of file + \ No newline at end of file diff --git a/assets/xmark.svg b/assets/xmark.svg index 340f479e..83bd5740 100644 --- a/assets/xmark.svg +++ b/assets/xmark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/components/layout/Layout.js b/components/layout/Layout.js index 2c097e41..0e4b2b61 100644 --- a/components/layout/Layout.js +++ b/components/layout/Layout.js @@ -1,4 +1,3 @@ -import React from 'react'; import Head from 'next/head'; import Header from 'components/layout/Header'; import Footer from 'components/layout/Footer'; @@ -10,9 +9,8 @@ export default function Layout({ title, children, header = true, footer = true } return ( <> - umami{title && ` - ${title}`} + {title ? `${title} | umami` : 'umami'} - {header &&
}
{children}
{footer &&