add members | phase 1

This commit is contained in:
Emily
2024-06-19 18:20:14 +02:00
parent 90d957c593
commit c1a15c8fc2
11 changed files with 259 additions and 6 deletions

View File

@@ -4,12 +4,18 @@ 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(),
@@ -28,7 +34,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

@@ -0,0 +1,83 @@
<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: '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 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 @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>
</UTable>
</div>
</div>
</template>

View File

@@ -3,6 +3,7 @@
definePageMeta({ layout: 'dashboard' }); definePageMeta({ layout: 'dashboard' });
const { projects, refresh } = useProjectsList(); const { projects, refresh } = useProjectsList();
const { guestProjects } = 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());
@@ -69,7 +70,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 +102,18 @@ 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>
</div> </div>
</div> </div>
<div class="px-10"> <div class="px-10">

View File

@@ -1,6 +1,7 @@
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) {
if (project_id == LITLYX_PROJECT_ID) { if (project_id == LITLYX_PROJECT_ID) {
@@ -8,7 +9,10 @@ export async function getUserProjectFromId(project_id: string, user: AuthContext
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 = await hasAccessToProject(user.id, project_id, project);
if (!hasAccess) return;
return project; return project;
} }

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

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