mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
add members | phase 1
This commit is contained in:
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
83
dashboard/pages/members.vue
Normal file
83
dashboard/pages/members.vue
Normal 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>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 };
|
||||||
|
|
||||||
|
});
|
||||||
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 { 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');
|
||||||
|
|
||||||
|
|||||||
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'];
|
||||||
|
}
|
||||||
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