mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add api keys
This commit is contained in:
@@ -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(() => {
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
47
dashboard/server/api/keys/create.post.ts
Normal file
47
dashboard/server/api/keys/create.post.ts
Normal 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();
|
||||
|
||||
});
|
||||
28
dashboard/server/api/keys/delete.delete.ts
Normal file
28
dashboard/server/api/keys/delete.delete.ts
Normal 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 };
|
||||
|
||||
});
|
||||
33
dashboard/server/api/keys/get_all.ts
Normal file
33
dashboard/server/api/keys/get_all.ts
Normal 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[];
|
||||
|
||||
});
|
||||
@@ -22,6 +22,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();
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
44
dashboard/server/api/v1/events.ts
Normal file
44
dashboard/server/api/v1/events.ts
Normal 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());
|
||||
|
||||
|
||||
});
|
||||
44
dashboard/server/api/v1/visits.ts
Normal file
44
dashboard/server/api/v1/visits.ts
Normal 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());
|
||||
|
||||
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
|
||||
20
shared/schema/ApiSettingsSchema.ts
Normal file
20
shared/schema/ApiSettingsSchema.ts
Normal 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);
|
||||
Reference in New Issue
Block a user