mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
Merge branch 'dev'
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
|
||||
|
||||
|
||||
LIB ---> Producer ---> Save to Redis stream
|
||||
|
||||
Broker ---> Read from redis stream ---> Process event ---> Save to DB
|
||||
@@ -1,6 +1,4 @@
|
||||
:root {
|
||||
--current-card-color: #1d1d1f;
|
||||
--card-color-1: #1d1d1f;
|
||||
--card-color-2: #1f1f1f;
|
||||
--card-color-3: #0f0f0f;
|
||||
--card-color: #1d1d1f;
|
||||
--bg-color: #151517;
|
||||
}
|
||||
@@ -50,10 +50,6 @@ const { isOpen, close } = useMenu();
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="debugMode" class="flex justify-center w-full">
|
||||
<ThemeSelector></ThemeSelector>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<div v-for="section of sections" class="flex flex-col gap-1">
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
function cardColor(val: number) {
|
||||
document.documentElement.style.setProperty(
|
||||
'--current-card-color',
|
||||
`var(--card-color-${val})`
|
||||
);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-2 bg-[#151517] py-3 px-8 rounded-lg">
|
||||
<div @click="cardColor(1)" class="card1 px-4 py-1 rounded-lg cursor-pointer">
|
||||
A
|
||||
</div>
|
||||
<div @click="cardColor(2)" class="card2 px-4 py-1 rounded-lg cursor-pointer">
|
||||
B
|
||||
</div>
|
||||
<div @click="cardColor(3)" class="card3 px-4 py-1 rounded-lg cursor-pointer">
|
||||
C
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped lang=scss>
|
||||
.card1 {
|
||||
background-color: var(--card-color-1) !important;
|
||||
}
|
||||
|
||||
.card2 {
|
||||
background-color: var(--card-color-2) !important;
|
||||
}
|
||||
|
||||
.card3 {
|
||||
background-color: var(--card-color-3) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -54,7 +54,7 @@ function openExternalLink(link: string) {
|
||||
|
||||
<div class="text-text flex flex-col items-start gap-4 w-full relative">
|
||||
|
||||
<div class="w-full h-full p-4 flex flex-col bg-menu rounded-xl gap-8 card-shadow">
|
||||
<div class="w-full h-full p-4 flex flex-col bg-card rounded-xl gap-8 card-shadow">
|
||||
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
@@ -31,7 +31,7 @@ const starterTierCardData = ref<PricingCardProp>({
|
||||
|
||||
const accelerationTierCardData = ref<PricingCardProp>({
|
||||
title: 'ACCELERATION',
|
||||
cost: '9.99',
|
||||
cost: '9,99',
|
||||
features: [
|
||||
"150K visits/events per month",
|
||||
"100 AI Interaction per month",
|
||||
@@ -50,7 +50,7 @@ const accelerationTierCardData = ref<PricingCardProp>({
|
||||
|
||||
const expansionTierCardData = ref<PricingCardProp>({
|
||||
title: 'EXPANSION',
|
||||
cost: '39.99',
|
||||
cost: '39,99',
|
||||
features: [
|
||||
"500K visits/events per month",
|
||||
"5000 AI Interaction per month",
|
||||
|
||||
@@ -4,17 +4,29 @@ const projects = useFetch<TProject[]>('/api/project/list', {
|
||||
key: 'projectslist', ...signHeaders()
|
||||
});
|
||||
|
||||
|
||||
|
||||
export function useProjectsList() {
|
||||
return { ...projects, projects: projects.data }
|
||||
}
|
||||
|
||||
const guestProjects = useFetch<TProject[]>('/api/project/list_guest', {
|
||||
key: 'guestProjectslist', ...signHeaders()
|
||||
});
|
||||
|
||||
export function useGuestProjectsList() {
|
||||
return { ...guestProjects, guestProjects: guestProjects.data }
|
||||
}
|
||||
|
||||
const activeProjectId = useFetch<string>(`/api/user/active_project`, {
|
||||
key: 'activeProjectId', ...signHeaders(),
|
||||
});
|
||||
|
||||
export const isGuest = computed(() => {
|
||||
if (!guestProjects.data.value) return false;
|
||||
const guestTarget = guestProjects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
|
||||
if (guestTarget) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
export function useActiveProjectId() {
|
||||
return { ...activeProjectId, pid: activeProjectId.data }
|
||||
}
|
||||
@@ -28,7 +40,10 @@ export function useActiveProject() {
|
||||
if (!projects.data.value) return;
|
||||
if (!activeProjectId.data.value) return;
|
||||
const target = projects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
|
||||
return target;
|
||||
if (target) return target;
|
||||
if (!guestProjects.data.value) return;
|
||||
const guestTarget = guestProjects.data.value.find(e => e._id.toString() == activeProjectId.data.value);
|
||||
return guestTarget;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const sections: Section[] = [
|
||||
title: 'General',
|
||||
entries: [
|
||||
{ label: 'Projects', icon: 'far fa-table-layout', to: '/project_selector' },
|
||||
{ label: 'Members', icon: 'far fa-users', to: '/members' },
|
||||
{ label: 'Admin', icon: 'fas fa-cat', adminOnly: true, to: '/admin' },
|
||||
]
|
||||
},
|
||||
|
||||
@@ -105,6 +105,18 @@ const defaultPrompts = [
|
||||
'How many events i got last week ?',
|
||||
]
|
||||
|
||||
async function deleteChat(chat_id: string) {
|
||||
if (!activeProject.value) return;
|
||||
const sure = confirm("Are you sure to delete the chat ?");
|
||||
if (!sure) return;
|
||||
if (currentChatId.value === chat_id) {
|
||||
currentChatId.value = "";
|
||||
currentChatMessages.value = [];
|
||||
}
|
||||
await $fetch(`/api/ai/${activeProject.value._id}/${chat_id}/delete`, signHeaders());
|
||||
await reloadChatsList();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -118,10 +130,13 @@ const defaultPrompts = [
|
||||
<div class="w-[10rem]">
|
||||
<img :src="'analyst.png'" class="w-full h-full">
|
||||
</div>
|
||||
<div class="poppins text-[1.2rem]">
|
||||
<div v-if="!isGuest" class="poppins text-[1.2rem]">
|
||||
How can i help you today?
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-6">
|
||||
<div v-if="isGuest" class="poppins text-[1.2rem]">
|
||||
Im not allowed to help guests :c
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mt-6" v-if="!isGuest">
|
||||
<div v-for="prompt of defaultPrompts" @click="currentText = prompt"
|
||||
class="bg-[#2f2f2f] hover:bg-[#424242] cursor-pointer p-4 rounded-lg poppins text-center">
|
||||
{{ prompt }}
|
||||
@@ -160,7 +175,7 @@ const defaultPrompts = [
|
||||
|
||||
|
||||
|
||||
<div class="flex gap-2 items-center absolute bottom-8 left-0 w-full px-10 xl:px-28">
|
||||
<div v-if="!isGuest" class="flex gap-2 items-center absolute bottom-8 left-0 w-full px-10 xl:px-28">
|
||||
<input @keydown="onKeyDown" v-model="currentText"
|
||||
class="bg-[#303030] w-full focus:outline-none px-4 py-2 rounded-lg" type="text">
|
||||
<div @click="sendMessage()"
|
||||
@@ -213,12 +228,17 @@ const defaultPrompts = [
|
||||
|
||||
<div class="overflow-y-auto">
|
||||
<div class="flex flex-col gap-2 px-2">
|
||||
<div @click="openChat(chat._id.toString())" v-for="chat of chatsList?.toReversed()"
|
||||
class="bg-menu px-4 py-3 cursor-pointer hover:bg-menu/80 poppins rounded-lg"
|
||||
<div class="flex items-center gap-4 w-full" v-for="chat of chatsList?.toReversed()">
|
||||
<i @click="deleteChat(chat._id.toString())"
|
||||
class="fas fa-trash hover:text-gray-300 cursor-pointer"></i>
|
||||
<div @click="openChat(chat._id.toString())"
|
||||
class="bg-menu px-4 py-3 w-full cursor-pointer hover:bg-menu/80 poppins rounded-lg"
|
||||
:class="{ '!bg-accent/60': chat._id.toString() === currentChatId }">
|
||||
{{ chat.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const eventsStackedSelectIndex = ref<number>(0);
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
<div class="bg-menu p-4 rounded-xl flex-[2] flex flex-col gap-10 h-full">
|
||||
<div class="bg-card p-4 rounded-xl flex-[2] flex flex-col gap-10 h-full">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="poppins font-semibold text-[1.4rem] text-text">
|
||||
Top events
|
||||
|
||||
116
dashboard/pages/members.vue
Normal file
116
dashboard/pages/members.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
const columns = [
|
||||
{ key: 'me', label: '' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'name', label: 'Name' },
|
||||
{ key: 'role', label: 'Role' },
|
||||
{ key: 'action', label: 'Actions' },
|
||||
// { key: 'pending', label: 'Pending' },
|
||||
]
|
||||
|
||||
const { data: members, refresh: refreshMembers } = useFetch('/api/project/members/list', signHeaders());
|
||||
|
||||
const showAddMember = ref<boolean>(false);
|
||||
|
||||
const addMemberEmail = ref<string>("");
|
||||
|
||||
async function kickMember(email: string) {
|
||||
|
||||
const sure = confirm('Are you sure to kick ' + email + ' ?');
|
||||
if (!sure) return;
|
||||
|
||||
try {
|
||||
|
||||
await $fetch('/api/project/members/kick', {
|
||||
method: 'POST',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ email }),
|
||||
onResponseError({ request, response, options }) {
|
||||
alert(response.statusText);
|
||||
}
|
||||
});
|
||||
|
||||
refreshMembers();
|
||||
|
||||
} catch (ex: any) { }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function addMember() {
|
||||
|
||||
if (addMemberEmail.value.length === 0) return;
|
||||
|
||||
try {
|
||||
|
||||
showAddMember.value = false;
|
||||
|
||||
await $fetch('/api/project/members/add', {
|
||||
method: 'POST',
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ email: addMemberEmail.value }),
|
||||
onResponseError({ request, response, options }) {
|
||||
alert(response.statusText);
|
||||
}
|
||||
});
|
||||
|
||||
addMemberEmail.value = '';
|
||||
|
||||
refreshMembers();
|
||||
|
||||
} catch (ex: any) { }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div class="home w-full h-full px-10 lg:px-6 overflow-y-auto pb-[12rem] md:pb-0 py-2">
|
||||
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<div v-if="!isGuest" @click="showAddMember = !showAddMember;"
|
||||
class="flex items-center gap-2 bg-menu w-fit px-3 py-2 rounded-lg hover:bg-menu/80 cursor-pointer">
|
||||
<i class="fas fa-plus"></i>
|
||||
<div> Add member </div>
|
||||
</div>
|
||||
|
||||
<div v-if="showAddMember" class="flex gap-4 items-center">
|
||||
<input v-model="addMemberEmail" class="focus:outline-none bg-menu px-4 py-1 rounded-lg" type="text"
|
||||
placeholder="user email">
|
||||
<div @click="addMember" class="bg-menu w-fit py-1 px-4 rounded-lg hover:bg-menu/80 cursor-pointer">
|
||||
Add
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UTable :rows="members || []" :columns="columns">
|
||||
<template #me-data="e">
|
||||
<i v-if="e.row.me" class="far fa-user"></i>
|
||||
<i v-if="!e.row.me"></i>
|
||||
</template>
|
||||
|
||||
<template #action-data="e" v-if="!isGuest">
|
||||
<div @click="kickMember(e.row.email)" v-if="e.row.role != 'OWNER'"
|
||||
class="text-red-500 hover:bg-black/20 cursor-pointer outline outline-[1px] outline-red-500 px-3 py-1 rounded-lg text-center">
|
||||
Kick
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</UTable>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
34
dashboard/pages/payment_ok.vue
Normal file
34
dashboard/pages/payment_ok.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
definePageMeta({ layout: 'none' });
|
||||
|
||||
const activeProject = useActiveProject();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div class="w-full h-full">
|
||||
|
||||
|
||||
<div class="flex items-center h-full flex-col gap-4">
|
||||
|
||||
<div class="text-accent mt-[20vh] poppins font-semibold text-[1.5rem]">
|
||||
Payment success
|
||||
</div>
|
||||
|
||||
<div class="poppins">
|
||||
We hope Lilyx can help you make better metrics-driven decision to help your business.
|
||||
</div>
|
||||
|
||||
<NuxtLink to="/?just_logged=true" class="text-accent mt-10 bg-menu px-6 py-2 rounded-lg hover:bg-black font-semibold poppins cursor-pointer">
|
||||
Go back to dashboard
|
||||
</NuxtLink>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -130,7 +130,7 @@ function getPremiumName(type: number) {
|
||||
<div class="poppins"> Expire date:</div>
|
||||
<div> {{ prettyExpireDate }}</div>
|
||||
</div>
|
||||
<div @click="onPlanUpgradeClick()"
|
||||
<div v-if="!isGuest" @click="onPlanUpgradeClick()"
|
||||
class="cursor-pointer flex items-center gap-2 text-[.9rem] text-white font-semibold bg-accent px-4 py-1 rounded-lg drop-shadow-[0_0_8px_#000000]">
|
||||
<div class="poppins"> Upgrade plan </div>
|
||||
<i class="fas fa-arrow-up-right"></i>
|
||||
@@ -168,7 +168,7 @@ function getPremiumName(type: number) {
|
||||
</div>
|
||||
|
||||
|
||||
<CardTitled title="Invoices" :sub="(invoices && invoices.length == 0) ? 'No invoices yet' : ''"
|
||||
<CardTitled v-if="!isGuest" title="Invoices" :sub="(invoices && invoices.length == 0) ? 'No invoices yet' : ''"
|
||||
class="p-4 mt-8 max-w-[72rem]">
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
|
||||
const { projects, refresh } = useProjectsList();
|
||||
const { guestProjects, refresh: refreshGuest } = useGuestProjectsList();
|
||||
const { pid } = useActiveProjectId();
|
||||
|
||||
const { data: maxProjects } = useFetch("/api/user/max_projects", signHeaders());
|
||||
@@ -35,6 +36,30 @@ async function deleteProject(projectId: string, projectName: string) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function leaveProject(projectId: string, projectName: string) {
|
||||
const sure = confirm(`Are you sure to leave the project ${projectName} ?`);
|
||||
if (!sure) return;
|
||||
|
||||
try {
|
||||
|
||||
await $fetch('/api/project/members/leave', signHeaders());
|
||||
|
||||
await refreshGuest();
|
||||
|
||||
if (pid.value == projectId) {
|
||||
const firstProjectId = projects.value?.[0]?._id.toString();
|
||||
if (firstProjectId) {
|
||||
await setActiveProject(firstProjectId);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (ex: any) {
|
||||
alert(ex.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { setToken } = useAccessToken();
|
||||
@@ -69,7 +94,7 @@ async function deleteAccount() {
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="text-text font-bold text-[1.5rem]"> Projects </div>
|
||||
<div class="text-text-sub/90 text-[1rem] font-semibold lato">
|
||||
{{ projects?.length ?? '-' }} / {{maxProjects || 3}}
|
||||
{{ projects?.length ?? '-' }} / {{ maxProjects || 3 }}
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink v-if="(projects?.length || 0) < (maxProjects || 3)" to="/project_creation"
|
||||
@@ -101,9 +126,22 @@ async function deleteAccount() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="e of guestProjects">
|
||||
<DashboardProjectSelectionCard class="outline outline-[2px] outline-yellow-200"
|
||||
@click="onProjectClick(e._id.toString())" :title="e.name" :active="pid == e._id.toString()"
|
||||
:subtitle="pid == e._id.toString() ? 'ATTIVO' : ''" :chip="''">
|
||||
</DashboardProjectSelectionCard>
|
||||
<div @click="leaveProject(e._id.toString(), e.name)"
|
||||
class="mt-4 rounded-lg bg-[#3a3a3b] hover:bg-[#4f4f50] cursor-pointer hover:text-red-500 flex items-center justify-center py-3">
|
||||
<i class="far fa-right-from-bracket"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="px-10">
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { AuthContext } from "./middleware/01-authorization";
|
||||
import { ProjectModel } from "~/../shared/schema/ProjectSchema";
|
||||
import { LITLYX_PROJECT_ID } from '@data/LITLYX'
|
||||
import { hasAccessToProject } from "./utils/hasAccessToProject";
|
||||
|
||||
export async function getUserProjectFromId(project_id: string, user: AuthContext | undefined) {
|
||||
export async function getUserProjectFromId(project_id: string, user: AuthContext | undefined, allowGuest: boolean = true) {
|
||||
if (project_id == LITLYX_PROJECT_ID) {
|
||||
const project = await ProjectModel.findOne({ _id: project_id });
|
||||
return project;
|
||||
} else {
|
||||
if (!user?.logged) return;
|
||||
const project = await ProjectModel.findOne({ _id: project_id, owner: user.id });
|
||||
const project = await ProjectModel.findById(project_id);
|
||||
if (!project) return;
|
||||
const [hasAccess, role] = await hasAccessToProject(user.id, project_id, project);
|
||||
if (!hasAccess) return;
|
||||
if (role === 'GUEST' && !allowGuest) return false;
|
||||
return project;
|
||||
}
|
||||
|
||||
|
||||
21
dashboard/server/api/ai/[project_id]/[chat_id]/delete.ts
Normal file
21
dashboard/server/api/ai/[project_id]/[chat_id]/delete.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { AiChatModel } from "@schema/ai/AiChatSchema";
|
||||
import { sendMessageOnChat } from "~/server/services/AiService";
|
||||
|
||||
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
if (!event.context.params) return;
|
||||
const chat_id = event.context.params['chat_id'];
|
||||
|
||||
const result = await AiChatModel.deleteOne({ _id: chat_id, project_id });
|
||||
return result.deletedCount > 0;
|
||||
});
|
||||
@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
const project = await getUserProjectFromId(project_id, user, false);
|
||||
if (!project) return;
|
||||
|
||||
// if (!user?.logged) return;
|
||||
|
||||
@@ -63,7 +63,7 @@ export default defineEventHandler(async event => {
|
||||
return {
|
||||
eventsCount: count[0].events,
|
||||
visitsCount: count[0].visits,
|
||||
sessionsVisitsCount: totalSessions + (sessionsVisitsCount?.[0]?.count || 0),
|
||||
sessionsVisitsCount: totalSessions || 0,
|
||||
avgSessionDuration,
|
||||
firstEventDate,
|
||||
firstViewDate,
|
||||
|
||||
@@ -8,8 +8,12 @@ export default defineEventHandler(async event => {
|
||||
if (!user?.logged) return;
|
||||
const project_id = getRequestProjectId(event);
|
||||
if (!project_id) return;
|
||||
const project = await ProjectModel.findOne({ _id: project_id, owner: user.id });
|
||||
const project = await ProjectModel.findOne({ _id: project_id });
|
||||
if (!project) return;
|
||||
|
||||
const [hasAccess] = await hasAccessToProject(user.id, project_id, project)
|
||||
if (!hasAccess) return;
|
||||
|
||||
const query = getQuery(event);
|
||||
|
||||
const { orderBy, order, page, limit, type } = query;
|
||||
|
||||
@@ -17,7 +17,7 @@ export default defineEventHandler(async event => {
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
const project = await getUserProjectFromId(project_id, user, false);
|
||||
if (!project) return;
|
||||
|
||||
if (!project.customer_id) return [];
|
||||
|
||||
25
dashboard/server/api/project/list_guest.ts
Normal file
25
dashboard/server/api/project/list_guest.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ProjectModel, TProject } from "@schema/ProjectSchema";
|
||||
import { TTeamMember, TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const userData = getRequestUser(event);
|
||||
if (!userData?.logged) return [];
|
||||
|
||||
|
||||
const members = await TeamMemberModel.find({
|
||||
user_id: userData.id
|
||||
});
|
||||
|
||||
const projects: TProject[] = [];
|
||||
|
||||
for (const member of members) {
|
||||
const project = await ProjectModel.findById(member.project_id);
|
||||
if (!project) continue;
|
||||
projects.push(project.toJSON());
|
||||
}
|
||||
|
||||
return projects;
|
||||
|
||||
|
||||
});
|
||||
38
dashboard/server/api/project/members/add.post.ts
Normal file
38
dashboard/server/api/project/members/add.post.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
|
||||
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 { email } = await readBody(event);
|
||||
|
||||
const targetUser = await UserModel.findOne({ email });
|
||||
if (!targetUser) return setResponseStatus(event, 400, 'No user with this email');
|
||||
|
||||
|
||||
await TeamMemberModel.create({
|
||||
project_id,
|
||||
user_id: targetUser.id,
|
||||
pending: true,
|
||||
role: 'GUEST'
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
|
||||
});
|
||||
32
dashboard/server/api/project/members/kick.post.ts
Normal file
32
dashboard/server/api/project/members/kick.post.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
|
||||
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 { email } = await readBody(event);
|
||||
|
||||
const user = await UserModel.findOne({ email });
|
||||
if (!user) return setResponseStatus(event, 400, 'Email not found');
|
||||
|
||||
await TeamMemberModel.deleteOne({ project_id, user_id: user.id });
|
||||
|
||||
return { ok: true }
|
||||
|
||||
});
|
||||
23
dashboard/server/api/project/members/leave.ts
Normal file
23
dashboard/server/api/project/members/leave.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
|
||||
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');
|
||||
|
||||
await TeamMemberModel.deleteOne({ project_id, user_id: userData.id });
|
||||
|
||||
return { ok: true }
|
||||
|
||||
});
|
||||
49
dashboard/server/api/project/members/list.ts
Normal file
49
dashboard/server/api/project/members/list.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
import { UserModel } from "@schema/UserSchema";
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
import StripeService from '~/server/services/StripeService';
|
||||
|
||||
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');
|
||||
|
||||
const owner = await UserModel.findById(project.owner);
|
||||
if (!owner) return setResponseStatus(event, 400, 'No owner');
|
||||
|
||||
const members = await TeamMemberModel.find({ project_id });
|
||||
|
||||
const result: { email: string, name: string, role: string, pending: boolean, me: boolean }[] = [];
|
||||
|
||||
result.push({
|
||||
email: owner.email,
|
||||
name: owner.name,
|
||||
role: 'OWNER',
|
||||
pending: false,
|
||||
me: userData.id === owner.id
|
||||
})
|
||||
|
||||
for (const member of members) {
|
||||
const userMember = await UserModel.findById(member.user_id);
|
||||
if (!userMember) continue;
|
||||
result.push({
|
||||
email: userMember.email,
|
||||
name: userMember.name,
|
||||
role: member.role,
|
||||
pending: member.pending,
|
||||
me: userData.id === userMember.id
|
||||
})
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ProjectModel } from "@schema/ProjectSchema";
|
||||
|
||||
import { UserSettingsModel } from "@schema/UserSettings";
|
||||
import { hasAccessToProject } from "~/server/utils/hasAccessToProject";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
@@ -12,7 +13,7 @@ export default defineEventHandler(async event => {
|
||||
|
||||
const { project_id } = getQuery(event);
|
||||
|
||||
const hasAccess = await ProjectModel.exists({ owner: userData.id, _id: project_id });
|
||||
const [hasAccess] = await hasAccessToProject(userData.id, project_id as string);
|
||||
|
||||
if (!hasAccess) return setResponseStatus(event, 400, 'No access to project');
|
||||
|
||||
|
||||
11
dashboard/server/utils/hasAccessToProject.ts
Normal file
11
dashboard/server/utils/hasAccessToProject.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ProjectModel, TProject } from "@schema/ProjectSchema";
|
||||
import { TeamMemberModel } from "@schema/TeamMemberSchema";
|
||||
|
||||
export async function hasAccessToProject(user_id: string, project_id: string, project?: TProject) {
|
||||
const targetProject = project || await ProjectModel.findById(project_id, { owner: true });
|
||||
if (!targetProject) return [false, 'NONE'];
|
||||
if (targetProject.owner.toString() === user_id) return [true, 'OWNER'];
|
||||
const members = await TeamMemberModel.find({ project_id });
|
||||
if (members.map(e => e.user_id.toString()).includes(user_id)) return [true, 'GUEST'];
|
||||
return [false, 'NONE'];
|
||||
}
|
||||
@@ -11,10 +11,10 @@ module.exports = {
|
||||
},
|
||||
colors: {
|
||||
card: {
|
||||
DEFAULT: 'var(--current-card-color)',
|
||||
DEFAULT: 'var(--card-color)',
|
||||
},
|
||||
bg: {
|
||||
DEFAULT: '#151517',
|
||||
DEFAULT: 'var(--bg-color)',
|
||||
},
|
||||
menu: {
|
||||
DEFAULT: '#1d1d1f'
|
||||
|
||||
22
shared/schema/TeamMemberSchema.ts
Normal file
22
shared/schema/TeamMemberSchema.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { model, Schema, Types } from 'mongoose';
|
||||
|
||||
export type TeamMemberRole = 'ADMIN' | 'GUEST';
|
||||
|
||||
export type TTeamMember = {
|
||||
_id: Schema.Types.ObjectId,
|
||||
project_id: Schema.Types.ObjectId,
|
||||
user_id: Schema.Types.ObjectId,
|
||||
role: TeamMemberRole,
|
||||
pending: boolean,
|
||||
created_at: Date,
|
||||
}
|
||||
|
||||
const TeamMemberSchema = new Schema<TTeamMember>({
|
||||
project_id: { type: Types.ObjectId, index: true },
|
||||
user_id: { type: Types.ObjectId, index: true },
|
||||
role: { type: String, required: true },
|
||||
pending: { type: Boolean, required: true },
|
||||
created_at: { type: Date, required: true, default: () => Date.now() },
|
||||
});
|
||||
|
||||
export const TeamMemberModel = model<TTeamMember>('team_members', TeamMemberSchema);
|
||||
Reference in New Issue
Block a user