diff --git a/dashboard/components/settings/General.vue b/dashboard/components/settings/General.vue index 5f28c5f..c1736b3 100644 --- a/dashboard/components/settings/General.vue +++ b/dashboard/components/settings/General.vue @@ -1,9 +1,11 @@ @@ -74,10 +148,31 @@ async function deleteProject() { Change + + + + + + + + + + + + Name: {{ apiKey.apiName }} + {{ apiKey.apiKey }} + + + + + + + + {{ activeProject?._id.toString() }} - + @@ -87,7 +182,7 @@ async function deleteProject() { ` }} - + diff --git a/dashboard/pages/index.vue b/dashboard/pages/index.vue index a40b126..1d918ce 100644 --- a/dashboard/pages/index.vue +++ b/dashboard/pages/index.vue @@ -24,6 +24,9 @@ const limitsInfo = ref<{ onMounted(async () => { if (route.query.just_logged) return location.href = '/'; limitsInfo.value = await $fetch("/api/project/limits_info", signHeaders()); + watch(activeProject, async () => { + limitsInfo.value = await $fetch("/api/project/limits_info", signHeaders()); + }); }); diff --git a/dashboard/server/api/keys/create.post.ts b/dashboard/server/api/keys/create.post.ts new file mode 100644 index 0000000..cde6c50 --- /dev/null +++ b/dashboard/server/api/keys/create.post.ts @@ -0,0 +1,47 @@ + +import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; +import { ApiSettingsModel, TApiSettings } from "@schema/ApiSettingsSchema"; +import { UserSettingsModel } from "@schema/UserSettings"; +import { ProjectModel } from "@schema/ProjectSchema"; + +import crypto from 'crypto'; + +function generateApiKey() { + return 'lit_' + crypto.randomBytes(6).toString('hex'); +} + +export default defineEventHandler(async event => { + + const body = await readBody(event); + + if (body.name.length == 0) return setResponseStatus(event, 400, 'name is required'); + + if (body.name.length < 3) return setResponseStatus(event, 400, 'name too short'); + if (body.name.length > 32) return setResponseStatus(event, 400, 'name too long'); + + const userData = getRequestUser(event); + if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); + + const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); + if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project'); + + const project_id = currentActiveProject.active_project_id; + + const project = await ProjectModel.findById(project_id); + if (!project) return setResponseStatus(event, 400, 'Project not found'); + + if (project.owner.toString() != userData.id) { + return setResponseStatus(event, 400, 'You are not the owner'); + } + + const key = generateApiKey(); + + const keyNumbers = await ApiSettingsModel.countDocuments({ project_id }); + + if (keyNumbers >= 5) return setResponseStatus(event, 400, 'Api key limit reached'); + + const newApiSettings = await ApiSettingsModel.create({ project_id, apiKey: key, apiName: body.name, created_at: Date.now(), usage: 0 }); + + return newApiSettings.toJSON(); + +}); \ No newline at end of file diff --git a/dashboard/server/api/keys/delete.delete.ts b/dashboard/server/api/keys/delete.delete.ts new file mode 100644 index 0000000..893b443 --- /dev/null +++ b/dashboard/server/api/keys/delete.delete.ts @@ -0,0 +1,28 @@ + +import { ApiSettingsModel } from "@schema/ApiSettingsSchema"; +import { UserSettingsModel } from "@schema/UserSettings"; +import { ProjectModel } from "@schema/ProjectSchema"; + +export default defineEventHandler(async event => { + + const body = await readBody(event); + + const userData = getRequestUser(event); + if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); + + const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); + if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project'); + + const project_id = currentActiveProject.active_project_id; + + const project = await ProjectModel.findById(project_id); + if (!project) return setResponseStatus(event, 400, 'Project not found'); + + if (project.owner.toString() != userData.id) { + return setResponseStatus(event, 400, 'You are not the owner'); + } + + const deletation = await ApiSettingsModel.deleteOne({ _id: body.api_id }); + return { ok: deletation.acknowledged }; + +}); \ No newline at end of file diff --git a/dashboard/server/api/keys/get_all.ts b/dashboard/server/api/keys/get_all.ts new file mode 100644 index 0000000..3f32e87 --- /dev/null +++ b/dashboard/server/api/keys/get_all.ts @@ -0,0 +1,33 @@ + +import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; +import { ApiSettingsModel, TApiSettings } from "@schema/ApiSettingsSchema"; +import { UserSettingsModel } from "@schema/UserSettings"; +import { ProjectModel } from "@schema/ProjectSchema"; + + +function cryptApiKeyName(apiSettings: TApiSettings): TApiSettings { + return { ...apiSettings, apiKey: apiSettings.apiKey.substring(0, 6) + '******' } +} + +export default defineEventHandler(async event => { + + const userData = getRequestUser(event); + if (!userData?.logged) return setResponseStatus(event, 400, 'NotLogged'); + + const currentActiveProject = await UserSettingsModel.findOne({ user_id: userData.id }); + if (!currentActiveProject) return setResponseStatus(event, 400, 'You need to select a project'); + + const project_id = currentActiveProject.active_project_id; + + const project = await ProjectModel.findById(project_id); + if (!project) return setResponseStatus(event, 400, 'Project not found'); + + if (project.owner.toString() != userData.id) { + return setResponseStatus(event, 400, 'You are not the owner'); + } + + const apiKeys = await ApiSettingsModel.find({ project_id }, { project_id: 0 }) + + return apiKeys.map(e => cryptApiKeyName(e.toJSON())) as TApiSettings[]; + +}); \ No newline at end of file diff --git a/dashboard/server/api/project/change_name.post.ts b/dashboard/server/api/project/change_name.post.ts index 5cc1d39..61a7554 100644 --- a/dashboard/server/api/project/change_name.post.ts +++ b/dashboard/server/api/project/change_name.post.ts @@ -21,6 +21,8 @@ export default defineEventHandler(async event => { } const { name } = await readBody(event); + + if (name.length == 0) return setResponseStatus(event, 400, 'name is required'); project.name = name; await project.save(); diff --git a/dashboard/server/api/project/generate_pdf.ts b/dashboard/server/api/project/generate_pdf.ts index 8152ee9..1a7a686 100644 --- a/dashboard/server/api/project/generate_pdf.ts +++ b/dashboard/server/api/project/generate_pdf.ts @@ -8,6 +8,7 @@ import { ProjectModel, TProject } from "@schema/ProjectSchema"; import { UserSettingsModel } from "@schema/UserSettings"; import { VisitModel } from '@schema/metrics/VisitSchema'; import { EventModel } from '@schema/metrics/EventSchema'; +import { ProjectSnapshotModel } from '@schema/ProjectSnapshot'; type PDF_Data = { @@ -114,8 +115,11 @@ export default defineEventHandler(async event => { const project = await ProjectModel.findById(project_id); if (!project) return setResponseStatus(event, 400, 'Project not found'); + const fromHeader = getHeader(event, 'x-from'); + const toHeader = getHeader(event, 'x-from'); - + const from = fromHeader; + const to = toHeader; const eventsCount = await EventModel.countDocuments({ project_id: project._id }); const visitsCount = await VisitModel.countDocuments({ project_id: project._id }); diff --git a/dashboard/server/api/v1/events.ts b/dashboard/server/api/v1/events.ts new file mode 100644 index 0000000..73a2f52 --- /dev/null +++ b/dashboard/server/api/v1/events.ts @@ -0,0 +1,44 @@ + +import { ApiSettingsModel } from '@schema/ApiSettingsSchema'; +import { EventModel } from '@schema/metrics/EventSchema'; + +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 [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 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()); + + +}); \ No newline at end of file diff --git a/dashboard/server/api/v1/visits.ts b/dashboard/server/api/v1/visits.ts new file mode 100644 index 0000000..ccbbefe --- /dev/null +++ b/dashboard/server/api/v1/visits.ts @@ -0,0 +1,44 @@ + +import { ApiSettingsModel } from '@schema/ApiSettingsSchema'; +import { VisitModel } from '@schema/metrics/VisitSchema'; + +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 [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 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 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()); + + +}); \ No newline at end of file diff --git a/dashboard/server/services/AiService.ts b/dashboard/server/services/AiService.ts index 39ab25b..f611cfe 100644 --- a/dashboard/server/services/AiService.ts +++ b/dashboard/server/services/AiService.ts @@ -127,7 +127,7 @@ export async function sendMessageOnChat(text: string, pid: string, initial_chat_ messages.push({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) }); await addMessageToChat({ tool_call_id: toolCall.id, role: "tool", content: JSON.stringify(functionResponse) }, chat_id); } - response = await openai.chat.completions.create({ model: 'gpt-3.5-turbo', messages, n: 1, tools }); + response = await openai.chat.completions.create({ model: 'gpt-4o', messages, n: 1, tools }); responseMessage = response.choices[0].message; toolCalls = responseMessage.tool_calls; diff --git a/shared/schema/ApiSettingsSchema.ts b/shared/schema/ApiSettingsSchema.ts new file mode 100644 index 0000000..61f8681 --- /dev/null +++ b/shared/schema/ApiSettingsSchema.ts @@ -0,0 +1,20 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TApiSettings = { + _id: Schema.Types.ObjectId, + project_id: Schema.Types.ObjectId, + apiKey: string, + apiName: string, + usage: number, + created_at: Date +} + +const ApiSettingsSchema = new Schema({ + project_id: { type: Types.ObjectId, index: 1 }, + apiKey: { type: String, required: true }, + apiName: { type: String, required: true }, + usage: { type: Number, default: 0, required: true, }, + created_at: { type: Date, default: () => Date.now() }, +}); + +export const ApiSettingsModel = model('api_settings', ApiSettingsSchema);