Merge branch 'dev'

This commit is contained in:
Emily
2024-06-20 14:34:12 +02:00
29 changed files with 484 additions and 81 deletions

View File

@@ -1,6 +0,0 @@
LIB ---> Producer ---> Save to Redis stream
Broker ---> Read from redis stream ---> Process event ---> Save to DB

View File

@@ -1,6 +1,4 @@
:root { :root {
--current-card-color: #1d1d1f; --card-color: #1d1d1f;
--card-color-1: #1d1d1f; --bg-color: #151517;
--card-color-2: #1f1f1f;
--card-color-3: #0f0f0f;
} }

View File

@@ -50,10 +50,6 @@ const { isOpen, close } = useMenu();
</div> </div>
<div v-if="debugMode" class="flex justify-center w-full">
<ThemeSelector></ThemeSelector>
</div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div v-for="section of sections" class="flex flex-col gap-1"> <div v-for="section of sections" class="flex flex-col gap-1">

View File

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

View File

@@ -54,7 +54,7 @@ function openExternalLink(link: string) {
<div class="text-text flex flex-col items-start gap-4 w-full relative"> <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 justify-between mb-3">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">

View File

@@ -31,7 +31,7 @@ const starterTierCardData = ref<PricingCardProp>({
const accelerationTierCardData = ref<PricingCardProp>({ const accelerationTierCardData = ref<PricingCardProp>({
title: 'ACCELERATION', title: 'ACCELERATION',
cost: '9.99', cost: '9,99',
features: [ features: [
"150K visits/events per month", "150K visits/events per month",
"100 AI Interaction per month", "100 AI Interaction per month",
@@ -50,7 +50,7 @@ const accelerationTierCardData = ref<PricingCardProp>({
const expansionTierCardData = ref<PricingCardProp>({ const expansionTierCardData = ref<PricingCardProp>({
title: 'EXPANSION', title: 'EXPANSION',
cost: '39.99', cost: '39,99',
features: [ features: [
"500K visits/events per month", "500K visits/events per month",
"5000 AI Interaction per month", "5000 AI Interaction per month",

View File

@@ -4,17 +4,29 @@ const projects = useFetch<TProject[]>('/api/project/list', {
key: 'projectslist', ...signHeaders() key: 'projectslist', ...signHeaders()
}); });
export function useProjectsList() { export function useProjectsList() {
return { ...projects, projects: projects.data } 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`, { const activeProjectId = useFetch<string>(`/api/user/active_project`, {
key: 'activeProjectId', ...signHeaders(), 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() { export function useActiveProjectId() {
return { ...activeProjectId, pid: activeProjectId.data } return { ...activeProjectId, pid: activeProjectId.data }
} }
@@ -28,7 +40,10 @@ export function useActiveProject() {
if (!projects.data.value) return; if (!projects.data.value) return;
if (!activeProjectId.data.value) return; if (!activeProjectId.data.value) return;
const target = projects.data.value.find(e => e._id.toString() == activeProjectId.data.value); 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;
}); });
} }

View File

@@ -12,6 +12,7 @@ const sections: Section[] = [
title: 'General', title: 'General',
entries: [ entries: [
{ label: 'Projects', icon: 'far fa-table-layout', to: '/project_selector' }, { 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' }, { label: 'Admin', icon: 'fas fa-cat', adminOnly: true, to: '/admin' },
] ]
}, },

View File

@@ -105,6 +105,18 @@ const defaultPrompts = [
'How many events i got last week ?', '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> </script>
<template> <template>
@@ -118,10 +130,13 @@ const defaultPrompts = [
<div class="w-[10rem]"> <div class="w-[10rem]">
<img :src="'analyst.png'" class="w-full h-full"> <img :src="'analyst.png'" class="w-full h-full">
</div> </div>
<div class="poppins text-[1.2rem]"> <div v-if="!isGuest" class="poppins text-[1.2rem]">
How can i help you today? How can i help you today?
</div> </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" <div v-for="prompt of defaultPrompts" @click="currentText = prompt"
class="bg-[#2f2f2f] hover:bg-[#424242] cursor-pointer p-4 rounded-lg poppins text-center"> class="bg-[#2f2f2f] hover:bg-[#424242] cursor-pointer p-4 rounded-lg poppins text-center">
{{ prompt }} {{ 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" <input @keydown="onKeyDown" v-model="currentText"
class="bg-[#303030] w-full focus:outline-none px-4 py-2 rounded-lg" type="text"> class="bg-[#303030] w-full focus:outline-none px-4 py-2 rounded-lg" type="text">
<div @click="sendMessage()" <div @click="sendMessage()"
@@ -213,11 +228,16 @@ const defaultPrompts = [
<div class="overflow-y-auto"> <div class="overflow-y-auto">
<div class="flex flex-col gap-2 px-2"> <div class="flex flex-col gap-2 px-2">
<div @click="openChat(chat._id.toString())" v-for="chat of chatsList?.toReversed()" <div class="flex items-center gap-4 w-full" v-for="chat of chatsList?.toReversed()">
class="bg-menu px-4 py-3 cursor-pointer hover:bg-menu/80 poppins rounded-lg" <i @click="deleteChat(chat._id.toString())"
:class="{ '!bg-accent/60': chat._id.toString() === currentChatId }"> class="fas fa-trash hover:text-gray-300 cursor-pointer"></i>
{{ chat.title }} <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> </div>
</div> </div>

View File

@@ -29,7 +29,7 @@ const eventsStackedSelectIndex = ref<number>(0);
</div> </div>
</CardTitled> </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="flex flex-col gap-1">
<div class="poppins font-semibold text-[1.4rem] text-text"> <div class="poppins font-semibold text-[1.4rem] text-text">
Top events Top events

116
dashboard/pages/members.vue Normal file
View 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>

View 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>

View File

@@ -130,7 +130,7 @@ function getPremiumName(type: number) {
<div class="poppins"> Expire date:</div> <div class="poppins"> Expire date:</div>
<div> {{ prettyExpireDate }}</div> <div> {{ prettyExpireDate }}</div>
</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]"> 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> <div class="poppins"> Upgrade plan </div>
<i class="fas fa-arrow-up-right"></i> <i class="fas fa-arrow-up-right"></i>
@@ -168,7 +168,7 @@ function getPremiumName(type: number) {
</div> </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]"> class="p-4 mt-8 max-w-[72rem]">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">

View File

@@ -3,6 +3,7 @@
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const { projects, refresh } = useProjectsList(); const { projects, refresh } = useProjectsList();
const { guestProjects, refresh: refreshGuest } = useGuestProjectsList();
const { pid } = useActiveProjectId(); const { pid } = useActiveProjectId();
const { data: maxProjects } = useFetch("/api/user/max_projects", signHeaders()); 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 router = useRouter();
const { setToken } = useAccessToken(); const { setToken } = useAccessToken();
@@ -69,7 +94,7 @@ async function deleteAccount() {
<div class="flex gap-4 items-center"> <div class="flex gap-4 items-center">
<div class="text-text font-bold text-[1.5rem]"> Projects </div> <div class="text-text font-bold text-[1.5rem]"> Projects </div>
<div class="text-text-sub/90 text-[1rem] font-semibold lato"> <div class="text-text-sub/90 text-[1rem] font-semibold lato">
{{ projects?.length ?? '-' }} / {{maxProjects || 3}} {{ projects?.length ?? '-' }} / {{ maxProjects || 3 }}
</div> </div>
</div> </div>
<NuxtLink v-if="(projects?.length || 0) < (maxProjects || 3)" to="/project_creation" <NuxtLink v-if="(projects?.length || 0) < (maxProjects || 3)" to="/project_creation"
@@ -101,9 +126,22 @@ async function deleteAccount() {
</div> </div>
</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> </div>
<div class="px-10"> <div class="px-10">

View File

@@ -1,14 +1,19 @@
import { AuthContext } from "./middleware/01-authorization"; import { AuthContext } from "./middleware/01-authorization";
import { ProjectModel } from "~/../shared/schema/ProjectSchema"; import { ProjectModel } from "~/../shared/schema/ProjectSchema";
import { LITLYX_PROJECT_ID } from '@data/LITLYX' 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) { if (project_id == LITLYX_PROJECT_ID) {
const project = await ProjectModel.findOne({ _id: project_id }); const project = await ProjectModel.findOne({ _id: project_id });
return project; return project;
} else { } else {
if (!user?.logged) return; 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; return project;
} }

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

View File

@@ -10,7 +10,7 @@ export default defineEventHandler(async event => {
if (!project_id) return; if (!project_id) return;
const user = getRequestUser(event); const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user); const project = await getUserProjectFromId(project_id, user, false);
if (!project) return; if (!project) return;
// if (!user?.logged) return; // if (!user?.logged) return;

View File

@@ -63,7 +63,7 @@ export default defineEventHandler(async event => {
return { return {
eventsCount: count[0].events, eventsCount: count[0].events,
visitsCount: count[0].visits, visitsCount: count[0].visits,
sessionsVisitsCount: totalSessions + (sessionsVisitsCount?.[0]?.count || 0), sessionsVisitsCount: totalSessions || 0,
avgSessionDuration, avgSessionDuration,
firstEventDate, firstEventDate,
firstViewDate, firstViewDate,

View File

@@ -8,8 +8,12 @@ export default defineEventHandler(async event => {
if (!user?.logged) return; if (!user?.logged) return;
const project_id = getRequestProjectId(event); const project_id = getRequestProjectId(event);
if (!project_id) return; 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; if (!project) return;
const [hasAccess] = await hasAccessToProject(user.id, project_id, project)
if (!hasAccess) return;
const query = getQuery(event); const query = getQuery(event);
const { orderBy, order, page, limit, type } = query; const { orderBy, order, page, limit, type } = query;

View File

@@ -17,7 +17,7 @@ export default defineEventHandler(async event => {
if (!project_id) return; if (!project_id) return;
const user = getRequestUser(event); const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user); const project = await getUserProjectFromId(project_id, user, false);
if (!project) return; if (!project) return;
if (!project.customer_id) return []; if (!project.customer_id) return [];

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

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

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

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

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

View File

@@ -2,6 +2,7 @@
import { ProjectModel } from "@schema/ProjectSchema"; import { ProjectModel } from "@schema/ProjectSchema";
import { UserSettingsModel } from "@schema/UserSettings"; import { UserSettingsModel } from "@schema/UserSettings";
import { hasAccessToProject } from "~/server/utils/hasAccessToProject";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -12,7 +13,7 @@ export default defineEventHandler(async event => {
const { project_id } = getQuery(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'); if (!hasAccess) return setResponseStatus(event, 400, 'No access to project');

View 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'];
}

View File

@@ -11,10 +11,10 @@ module.exports = {
}, },
colors: { colors: {
card: { card: {
DEFAULT: 'var(--current-card-color)', DEFAULT: 'var(--card-color)',
}, },
bg: { bg: {
DEFAULT: '#151517', DEFAULT: 'var(--bg-color)',
}, },
menu: { menu: {
DEFAULT: '#1d1d1f' DEFAULT: '#1d1d1f'

View 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);