diff --git a/dashboard/composables/Projects.ts b/dashboard/composables/Projects.ts index 3e84840..152c606 100644 --- a/dashboard/composables/Projects.ts +++ b/dashboard/composables/Projects.ts @@ -4,12 +4,18 @@ const projects = useFetch('/api/project/list', { key: 'projectslist', ...signHeaders() }); - - export function useProjectsList() { return { ...projects, projects: projects.data } } +const guestProjects = useFetch('/api/project/list_guest', { + key: 'guestProjectslist', ...signHeaders() +}); + +export function useGuestProjectsList() { + return { ...guestProjects, guestProjects: guestProjects.data } +} + const activeProjectId = useFetch(`/api/user/active_project`, { key: 'activeProjectId', ...signHeaders(), @@ -28,7 +34,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; }); } diff --git a/dashboard/layouts/dashboard.vue b/dashboard/layouts/dashboard.vue index 4f2e7fd..14cd79f 100644 --- a/dashboard/layouts/dashboard.vue +++ b/dashboard/layouts/dashboard.vue @@ -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' }, ] }, diff --git a/dashboard/pages/members.vue b/dashboard/pages/members.vue new file mode 100644 index 0000000..9361bbc --- /dev/null +++ b/dashboard/pages/members.vue @@ -0,0 +1,83 @@ + + + + diff --git a/dashboard/pages/project_selector.vue b/dashboard/pages/project_selector.vue index acaf6c6..0f50f2f 100644 --- a/dashboard/pages/project_selector.vue +++ b/dashboard/pages/project_selector.vue @@ -3,6 +3,7 @@ definePageMeta({ layout: 'dashboard' }); const { projects, refresh } = useProjectsList(); +const { guestProjects } = useGuestProjectsList(); const { pid } = useActiveProjectId(); const { data: maxProjects } = useFetch("/api/user/max_projects", signHeaders()); @@ -69,7 +70,7 @@ async function deleteAccount() {
Projects
- {{ projects?.length ?? '-' }} / {{maxProjects || 3}} + {{ projects?.length ?? '-' }} / {{ maxProjects || 3 }}
+
+ + +
+ + +
diff --git a/dashboard/server/LIVE_DEMO_DATA.ts b/dashboard/server/LIVE_DEMO_DATA.ts index 22652ed..507f577 100644 --- a/dashboard/server/LIVE_DEMO_DATA.ts +++ b/dashboard/server/LIVE_DEMO_DATA.ts @@ -1,6 +1,7 @@ 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) { if (project_id == LITLYX_PROJECT_ID) { @@ -8,7 +9,10 @@ export async function getUserProjectFromId(project_id: string, user: AuthContext 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 = await hasAccessToProject(user.id, project_id, project); + if (!hasAccess) return; return project; } diff --git a/dashboard/server/api/project/list_guest.ts b/dashboard/server/api/project/list_guest.ts new file mode 100644 index 0000000..e6f6fd7 --- /dev/null +++ b/dashboard/server/api/project/list_guest.ts @@ -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; + + +}); \ No newline at end of file diff --git a/dashboard/server/api/project/members/add.post.ts b/dashboard/server/api/project/members/add.post.ts new file mode 100644 index 0000000..5112fa6 --- /dev/null +++ b/dashboard/server/api/project/members/add.post.ts @@ -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 }; + +}); \ No newline at end of file diff --git a/dashboard/server/api/project/members/list.ts b/dashboard/server/api/project/members/list.ts new file mode 100644 index 0000000..1002fc8 --- /dev/null +++ b/dashboard/server/api/project/members/list.ts @@ -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; + +}); \ No newline at end of file diff --git a/dashboard/server/api/user/set_active_project.ts b/dashboard/server/api/user/set_active_project.ts index 0697a4b..cea5b7d 100644 --- a/dashboard/server/api/user/set_active_project.ts +++ b/dashboard/server/api/user/set_active_project.ts @@ -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'); diff --git a/dashboard/server/utils/hasAccessToProject.ts b/dashboard/server/utils/hasAccessToProject.ts new file mode 100644 index 0000000..cc54837 --- /dev/null +++ b/dashboard/server/utils/hasAccessToProject.ts @@ -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']; +} \ No newline at end of file diff --git a/shared/schema/TeamMemberSchema.ts b/shared/schema/TeamMemberSchema.ts new file mode 100644 index 0000000..11d5a84 --- /dev/null +++ b/shared/schema/TeamMemberSchema.ts @@ -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({ + 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('team_members', TeamMemberSchema); \ No newline at end of file