From 5172ad4f4deb5ba493b443cc2c259bc0c5cecd5f Mon Sep 17 00:00:00 2001 From: Emily Date: Mon, 9 Sep 2024 14:43:27 +0200 Subject: [PATCH] add api support --- dashboard/server/api/v1/events.post.ts | 27 ++++++++ dashboard/server/api/v1/events.ts | 41 ++++-------- dashboard/server/api/v1/visits.post.ts | 28 ++++++++ dashboard/server/api/v1/visits.ts | 37 +++-------- dashboard/server/services/ApiService.ts | 86 +++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 dashboard/server/api/v1/events.post.ts create mode 100644 dashboard/server/api/v1/visits.post.ts create mode 100644 dashboard/server/services/ApiService.ts diff --git a/dashboard/server/api/v1/events.post.ts b/dashboard/server/api/v1/events.post.ts new file mode 100644 index 0000000..e580d37 --- /dev/null +++ b/dashboard/server/api/v1/events.post.ts @@ -0,0 +1,27 @@ + +import { checkApiKey, checkAuthorization, eventsListApi } from '~/server/services/ApiService'; + + +export default defineEventHandler(async event => { + + const { rows, from, to, limit } = await readBody(event); + + const token = checkAuthorization(event); + if (!token) return; + + const apiKeyResult = await checkApiKey(token); + if (!apiKeyResult.ok) return setResponseStatus(event, 401, 'ApiKey not valid'); + + if (!rows) return setResponseStatus(event, 400, 'rows is required'); + if (!Array.isArray(rows)) return setResponseStatus(event, 400, 'rows must be an array'); + if (rows.length == 0) return setResponseStatus(event, 400, 'rows cannot be empty'); + + if (Array.isArray(from)) return setResponseStatus(event, 400, 'Only one "from" is allowed'); + if (Array.isArray(to)) return setResponseStatus(event, 400, 'Only one "to" is allowed'); + + const result = await eventsListApi(apiKeyResult.data.apiKey, apiKeyResult.data.project_id.toString(), rows, limit as string, from as string, to as string); + + if (result.ok) return result; + return setResponseStatus(event, result.code, result.error); + +}); \ No newline at end of file diff --git a/dashboard/server/api/v1/events.ts b/dashboard/server/api/v1/events.ts index 73a2f52..defd321 100644 --- a/dashboard/server/api/v1/events.ts +++ b/dashboard/server/api/v1/events.ts @@ -1,44 +1,25 @@ -import { ApiSettingsModel } from '@schema/ApiSettingsSchema'; -import { EventModel } from '@schema/metrics/EventSchema'; +import { checkApiKey, checkAuthorization, eventsListApi, } from '~/server/services/ApiService'; + export default defineEventHandler(async event => { const { row, from, to, limit } = getQuery(event); - const authorization = getHeader(event, 'Authorization'); - if (!authorization) return setResponseStatus(event, 403, 'Authorization is required'); + const token = checkAuthorization(event); + if (!token) return; - const [type, token] = authorization.split(' '); - if (type != 'Bearer') return setResponseStatus(event, 401, 'Malformed authorization'); - - const apiSettings = await ApiSettingsModel.findOne({ apiKey: token }); - if (!apiSettings) return setResponseStatus(event, 401, 'ApiKey not valid'); - - if (!row) return setResponseStatus(event, 400, 'row is required'); + const apiKeyResult = await checkApiKey(token); + if (!apiKeyResult.ok) return setResponseStatus(event, 401, 'ApiKey not valid'); + if (Array.isArray(from)) return setResponseStatus(event, 400, 'Only one "from" is allowed'); + if (Array.isArray(to)) return setResponseStatus(event, 400, 'Only one "to" is allowed'); const rows: string[] = Array.isArray(row) ? row as string[] : [row as string]; - const projection: any = {}; - - for (const row of rows) { - projection[row] = 1; - } - - const limitNumber = parseInt((limit as string)); - - const limitValue = isNaN(limitNumber) ? 100 : limitNumber; - - const visits = await EventModel.find({ - project_id: apiSettings.project_id, - created_at: { - $gte: from || new Date(2023, 0), - $lte: to || new Date(3000, 0) - } - }, { _id: 0, ...projection }, { limit: limitValue }); - - return visits.map(e => e.toJSON()); + const result = await eventsListApi(apiKeyResult.data.apiKey, apiKeyResult.data.project_id.toString(), rows, limit as string, from as string, to as string); + if (result.ok) return result; + return setResponseStatus(event, result.code, result.error); }); \ No newline at end of file diff --git a/dashboard/server/api/v1/visits.post.ts b/dashboard/server/api/v1/visits.post.ts new file mode 100644 index 0000000..509832d --- /dev/null +++ b/dashboard/server/api/v1/visits.post.ts @@ -0,0 +1,28 @@ + +import { checkApiKey, checkAuthorization } from '~/server/services/ApiService'; +import { visitsListApi } from '../../services/ApiService'; + + +export default defineEventHandler(async event => { + + const { rows, from, to, limit } = await readBody(event); + + const token = checkAuthorization(event); + if (!token) return; + + const apiKeyResult = await checkApiKey(token); + if (!apiKeyResult.ok) return setResponseStatus(event, 401, 'ApiKey not valid'); + + if (!rows) return setResponseStatus(event, 400, 'rows is required'); + if (!Array.isArray(rows)) return setResponseStatus(event, 400, 'rows must be an array'); + if (rows.length == 0) return setResponseStatus(event, 400, 'rows cannot be empty'); + + if (Array.isArray(from)) return setResponseStatus(event, 400, 'Only one "from" is allowed'); + if (Array.isArray(to)) return setResponseStatus(event, 400, 'Only one "to" is allowed'); + + const result = await visitsListApi(apiKeyResult.data.apiKey, apiKeyResult.data.project_id.toString(), rows, limit as string, from as string, to as string); + + if (result.ok) return result; + return setResponseStatus(event, result.code, result.error); + +}); \ No newline at end of file diff --git a/dashboard/server/api/v1/visits.ts b/dashboard/server/api/v1/visits.ts index ccbbefe..309370c 100644 --- a/dashboard/server/api/v1/visits.ts +++ b/dashboard/server/api/v1/visits.ts @@ -1,44 +1,27 @@ import { ApiSettingsModel } from '@schema/ApiSettingsSchema'; import { VisitModel } from '@schema/metrics/VisitSchema'; +import { checkApiKey, checkAuthorization, visitsListApi } from '~/server/services/ApiService'; export default defineEventHandler(async event => { const { row, from, to, limit } = getQuery(event); - const authorization = getHeader(event, 'Authorization'); - if (!authorization) return setResponseStatus(event, 403, 'Authorization is required'); + const token = checkAuthorization(event); + if (!token) return; - const [type, token] = authorization.split(' '); - if (type != 'Bearer') return setResponseStatus(event, 401, 'Malformed authorization'); - - const apiSettings = await ApiSettingsModel.findOne({ apiKey: token }); - if (!apiSettings) return setResponseStatus(event, 401, 'ApiKey not valid'); - - if (!row) return setResponseStatus(event, 400, 'row is required'); + const apiKeyResult = await checkApiKey(token); + if (!apiKeyResult.ok) return setResponseStatus(event, 401, 'ApiKey not valid'); + if (Array.isArray(from)) return setResponseStatus(event, 400, 'Only one "from" is allowed'); + if (Array.isArray(to)) return setResponseStatus(event, 400, 'Only one "to" is allowed'); const rows: string[] = Array.isArray(row) ? row as string[] : [row as string]; - const projection: any = {}; + const result = await visitsListApi(apiKeyResult.data.apiKey, apiKeyResult.data.project_id.toString(), rows, limit as string, from as string, to as string); - for (const row of rows) { - projection[row] = 1; - } - - const limitNumber = parseInt((limit as string)); - - const limitValue = isNaN(limitNumber) ? 100 : limitNumber; - - const visits = await VisitModel.find({ - project_id: apiSettings.project_id, - created_at: { - $gte: from || new Date(2023, 0), - $lte: to || new Date(3000, 0) - } - }, { _id: 0, ...projection }, { limit: limitValue }); - - return visits.map(e => e.toJSON()); + if (result.ok) return result; + return setResponseStatus(event, result.code, result.error); }); \ No newline at end of file diff --git a/dashboard/server/services/ApiService.ts b/dashboard/server/services/ApiService.ts new file mode 100644 index 0000000..8bce8da --- /dev/null +++ b/dashboard/server/services/ApiService.ts @@ -0,0 +1,86 @@ + +import { ApiSettingsModel, TApiSettings } from '@schema/ApiSettingsSchema'; +import { EventModel } from '@schema/metrics/EventSchema'; +import { VisitModel } from '@schema/metrics/VisitSchema'; +import type { H3Event, EventHandlerRequest } from 'h3' + +export function checkAuthorization(event: H3Event) { + const authorization = getHeader(event, 'Authorization'); + if (!authorization) return setResponseStatus(event, 403, 'Authorization is required'); + + const [type, token] = authorization.split(' '); + if (type != 'Bearer') return setResponseStatus(event, 401, 'Malformed authorization'); + + return token; +} + +export type CheckApiKeyResult = { ok: false } | { ok: true, data: TApiSettings }; + +export async function checkApiKey(apiKey: string): Promise { + const apiSettings = await ApiSettingsModel.findOne({ apiKey }); + if (!apiSettings) return { ok: false } + return { ok: true, data: apiSettings } +} + +async function incrementApiUsage(apiKey: string, value: number) { + await ApiSettingsModel.updateOne({ apiKey }, { $inc: { usage: value } }); +} + +async function checkApiUsage(apiKey: string) { + const data = await ApiSettingsModel.findOne({ apiKey }, { usage: 1 }); + if (!data) return false; + if (data.usage > 100000) return false; + return true; +} + +export type ApiResult = { ok: true, data: any } | { ok: false, code: number, error: string } + +export async function eventsListApi(apiKey: string, project_id: string, rows: string[], limit?: number | string, from?: string, to?: string): Promise { + + const canMakeRequest = await checkApiUsage(apiKey); + + if (!canMakeRequest) return { ok: false, code: 429, error: 'Api limit reached (100.000)' } + + const projection = Object.fromEntries(rows.map(e => [e, 1])); + + const limitNumber = parseInt((limit?.toString() as string)); + const limitValue = isNaN(limitNumber) ? 100 : limitNumber; + + const events = await EventModel.find({ + project_id, + created_at: { + $gte: from || new Date(2023, 0), + $lte: to || new Date(3000, 0) + } + }, { _id: 0, ...projection }, { limit: limitValue }); + + await incrementApiUsage(apiKey, events.length); + + return { ok: true, data: events.map(e => e.toJSON()) } + +} + +export async function visitsListApi(apiKey: string, project_id: string, rows: string[], limit?: number | string, from?: string, to?: string): Promise { + + const canMakeRequest = await checkApiUsage(apiKey); + + if (!canMakeRequest) return { ok: false, code: 429, error: 'Api limit reached (100.000)' } + + const projection = Object.fromEntries(rows.map(e => [e, 1])); + + const limitNumber = parseInt((limit?.toString() as string)); + const limitValue = isNaN(limitNumber) ? 100 : limitNumber; + + const visits = await VisitModel.find({ + project_id, + created_at: { + $gte: from || new Date(2023, 0), + $lte: to || new Date(3000, 0) + } + }, { _id: 0, ...projection }, { limit: limitValue }); + + await incrementApiUsage(apiKey, visits.length); + + return { ok: true, data: visits.map(e => e.toJSON()) }; + +} \ No newline at end of file