add api keys

This commit is contained in:
Emily
2024-09-08 15:51:03 +02:00
parent 73739dde9d
commit be45448288
11 changed files with 325 additions and 5 deletions

View File

@@ -1,9 +1,11 @@
<script lang="ts" setup>
import type { TApiSettings } from '@schema/ApiSettingsSchema';
import type { SettingsTemplateEntry } from './Template.vue';
const entries: SettingsTemplateEntry[] = [
{ id: 'pname', title: 'Name', text: 'Project name' },
{ id: 'api', title: 'ApiKeys', text: 'Manage your authorization token' },
{ id: 'pid', title: 'Id', text: 'Project id' },
{ id: 'pscript', title: 'Script', text: 'Universal javascript integration' },
{ id: 'pdelete', title: 'Delete', text: 'Delete current project' },
@@ -12,8 +14,54 @@ const entries: SettingsTemplateEntry[] = [
const activeProject = useActiveProject();
const projectNameInputVal = ref<string>(activeProject.value?.name || '');
const apiKeys = ref<TApiSettings[]>([]);
const newApiKeyName = ref<string>('');
async function updateApiKeys() {
newApiKeyName.value = '';
apiKeys.value = await $fetch<TApiSettings[]>('/api/keys/get_all', signHeaders());
}
async function createApiKey() {
try {
const res = await $fetch<TApiSettings>('/api/keys/create', {
method: 'POST', ...signHeaders({
'Content-Type': 'application/json'
}),
body: JSON.stringify({ name: newApiKeyName.value })
});
apiKeys.value.push(res);
newApiKeyName.value = '';
} catch (ex: any) {
alert(ex.message);
}
}
async function deleteApiKey(api_id: string) {
try {
const res = await $fetch<TApiSettings>('/api/keys/delete', {
method: 'DELETE', ...signHeaders({
'Content-Type': 'application/json'
}),
body: JSON.stringify({ api_id })
});
newApiKeyName.value = '';
await updateApiKeys();
} catch (ex: any) {
alert(ex.message);
}
}
onMounted(() => {
updateApiKeys();
})
watch(activeProject, () => {
projectNameInputVal.value = activeProject.value?.name || "";
updateApiKeys();
})
const canChange = computed(() => {
@@ -47,7 +95,7 @@ async function deleteProject() {
const projectsList = useProjectsList()
await projectsList.refresh();
const firstProjectId = projectsList.data.value?.[0]?._id.toString();
if (firstProjectId) {
await setActiveProject(firstProjectId);
@@ -61,6 +109,32 @@ async function deleteProject() {
}
const { createAlert } = useAlert()
function copyScript() {
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
const createScriptText = () => {
return [
'<script defer ',
`data-project="${activeProject.value?._id}" `,
'src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></',
'script>'
].join('')
}
navigator.clipboard.writeText(createScriptText());
createAlert('Success', 'Script copied successfully.', 'far fa-circle-check', 5000);
}
function copyProjectId() {
if (!navigator.clipboard) alert('You can\'t copy in HTTP');
navigator.clipboard.writeText(activeProject.value?._id?.toString() || '');
createAlert('Success', 'Project id copied successfully.', 'far fa-circle-check', 5000);
}
</script>
@@ -74,10 +148,31 @@ async function deleteProject() {
<LyxUiButton @click="changeProjectName()" :disabled="!canChange" type="primary"> Change </LyxUiButton>
</div>
</template>
<template #api>
<div class="flex items-center gap-4" v-if="apiKeys && apiKeys.length < 5">
<LyxUiInput class="grow px-4 py-2" placeholder="ApiKeyName" v-model="newApiKeyName"></LyxUiInput>
<LyxUiButton @click="createApiKey()" :disabled="newApiKeyName.length < 3" type="primary">
<i class="far fa-plus"></i>
</LyxUiButton>
</div>
<LyxUiCard v-if="apiKeys && apiKeys.length > 0" class="w-full flex flex-col gap-4 items-center mt-4">
<div v-for="apiKey of apiKeys" class="flex flex-col w-full">
<div class="flex gap-8 items-center">
<div class="grow">Name: {{ apiKey.apiName }}</div>
<div>{{ apiKey.apiKey }}</div>
<div class="flex justify-end">
<i class="far fa-trash cursor-pointer" @click="deleteApiKey(apiKey._id.toString())"></i>
</div>
</div>
</div>
</LyxUiCard>
</template>
<template #pid>
<LyxUiCard class="w-full flex items-center">
<div class="grow">{{ activeProject?._id.toString() }}</div>
<div><i class="far fa-copy"></i></div>
<div><i class="far fa-copy" @click="copyProjectId()"></i></div>
</LyxUiCard>
</template>
<template #pscript>
@@ -87,7 +182,7 @@ async function deleteProject() {
<script defer data-project="${activeProject?._id}"
src="https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js"></script>` }}
</div>
<div><i class="far fa-copy"></i></div>
<div><i class="far fa-copy" @click="copyScript()"></i></div>
</LyxUiCard>
</template>
<template #pdelete>

View File

@@ -24,6 +24,9 @@ const limitsInfo = ref<{
onMounted(async () => {
if (route.query.just_logged) return location.href = '/';
limitsInfo.value = await $fetch<any>("/api/project/limits_info", signHeaders());
watch(activeProject, async () => {
limitsInfo.value = await $fetch<any>("/api/project/limits_info", signHeaders());
});
});

View File

@@ -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();
});

View File

@@ -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 };
});

View File

@@ -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[];
});

View File

@@ -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();

View File

@@ -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 });

View File

@@ -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());
});

View File

@@ -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());
});

View File

@@ -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;

View File

@@ -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<TApiSettings>({
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<TApiSettings>('api_settings', ApiSettingsSchema);