Skip to content

Commit

Permalink
First attempts at claim ownership and project kebab
Browse files Browse the repository at this point in the history
  • Loading branch information
FyreByrd committed Jan 27, 2025
1 parent 95804ca commit 0ab2b2e
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 81 deletions.
21 changes: 14 additions & 7 deletions source/SIL.AppBuilder.Portal/common/databaseProxy/Projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,19 @@ async function validateProjectBase(orgId: number, groupId: number, ownerId: numb
// the relevant data is supplied. If it isn't, then this is an update
// and the data was valid already, or PostgreSQL will catch it
return !!(
orgId === (await prisma.groups.findUnique({ where: { Id: groupId } }))?.OwnerId &&
((await prisma.groupMemberships.count({
where: { UserId: ownerId, GroupId: groupId }
})) &&
(await prisma.organizationMemberships.count({
where: { UserId: ownerId, OrganizationId: orgId }
})) || (await prisma.userRoles.count({ where: { RoleId: RoleId.SuperAdmin }})))
// project group must be owned by project org
(
orgId === (await prisma.groups.findUnique({ where: { Id: groupId } }))?.OwnerId &&
// owner must be a member of project group
(((await prisma.groupMemberships.count({
where: { UserId: ownerId, GroupId: groupId }
})) &&
// owner must be a member of project org
(await prisma.organizationMemberships.count({
where: { UserId: ownerId, OrganizationId: orgId }
}))) ||
// disregard owner restrictions if owner is Super Admin
(await prisma.userRoles.count({ where: { RoleId: RoleId.SuperAdmin } })))
)
);
}
16 changes: 16 additions & 0 deletions source/SIL.AppBuilder.Portal/src/lib/projects/common.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,19 @@ export async function verifyCanCreateProject(user: Session, orgId: number) {
roles.includes(RoleId.SuperAdmin)
);
}

export async function userGroupsForOrg(userId: number, orgId: number) {
return prisma.groupMemberships.findMany({
where: {
UserId: userId,
Group: {
is: {
OwnerId: orgId
}
}
},
select: {
GroupId: true
}
});
}
33 changes: 28 additions & 5 deletions source/SIL.AppBuilder.Portal/src/lib/projects/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function pruneProjects(
Language,
Owner: { Name: OwnerName, Id: OwnerId },
Organization: { Name: OrganizationName },
Group: { Name: GroupName },
Group: { Name: GroupName, Id: GroupId },
DateActive,
DateUpdated,
DateArchived,
Expand All @@ -39,6 +39,7 @@ export function pruneProjects(
OwnerName,
OrganizationName,
GroupName,
GroupId,
DateUpdated,
DateActive,
DateArchived,
Expand Down Expand Up @@ -106,7 +107,10 @@ export const langtagRegex = new RegExp(
const projectSchemaBase = v.object({
Name: v.pipe(v.string(), v.minLength(1)),
Description: v.optional(v.string()),
Language: v.pipe(v.string(), v.regex(langtagRegex, (issue) => `Invalid BCP 47 Language Tag: ${issue.input}`)),
Language: v.pipe(
v.string(),
v.regex(langtagRegex, (issue) => `Invalid BCP 47 Language Tag: ${issue.input}`)
),
IsPublic: v.boolean()
});

Expand Down Expand Up @@ -138,13 +142,32 @@ export const importJSONSchema = v.object({
)
});

export function canModifyProject(user: Session, projectOwnerId: number, organizationId: number) {
if (projectOwnerId === user.user.userId) return true;
export function canModifyProject(
session: Session | null | undefined,
projectOwnerId: number,
organizationId: number
) {
if (projectOwnerId === session?.user.userId) return true;
if (
user.user.roles.find(
session?.user.roles.find(
(r) => r[1] === RoleId.SuperAdmin || (r[1] === RoleId.OrgAdmin && r[0] === organizationId)
)
)
return true;
return false;
}

export function canClaimProject(
session: Session | null | undefined,
projectOwnerId: number,
organizationId: number,
projectGroupId: number,
userGroupIds: number[]
) {
if (session?.user.userId === projectOwnerId) return false;
if (session?.user.roles.find((r) => r[1] === RoleId.SuperAdmin)) return true;
return (
canModifyProject(session, projectOwnerId, organizationId) &&
userGroupIds.includes(projectGroupId)
);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<script lang="ts">
import IconContainer from '$lib/components/IconContainer.svelte';
import { getIcon } from '$lib/icons/productDefinitionIcon';
import * as m from '$lib/paraglide/messages';
import { getTimeDateString } from '$lib/timeUtils';
import type { PrunedProject } from '../common';
import IconContainer from '$lib/components/IconContainer.svelte';
export let project: PrunedProject;
</script>

<div class="rounded-md bg-base-300 border border-slate-400 my-4 overflow-hidden w-full">
<div class="p-4 pb-2 w-full">
<span class="flex flex-row">
<slot name="checkbox" />
<slot name="select" />
<a href="/projects/{project.Id}">
<b class="[color:#55f]">
{project.Name}
Expand All @@ -28,9 +28,7 @@
{project.Language}
</span>
</span>

<!-- Removed on request from Chris -->
<!-- <IconContainer icon="charm:menu-kebab" width={20} class="inline float-right" /> -->
<slot name="actions" />
</span>
<div class="flex flex-wrap justify-between">
<div class="mr-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { canModifyProject, projectSearchSchema, pruneProjects } from '$lib/projects/common';
import { projectFilter } from '$lib/projects/common.server';
import {
canClaimProject,
canModifyProject,
projectSearchSchema,
pruneProjects
} from '$lib/projects/common';
import { projectFilter, userGroupsForOrg } from '$lib/projects/common.server';
import { idSchema } from '$lib/valibot';
import type { Prisma } from '@prisma/client';
import { error, redirect, type Actions } from '@sveltejs/kit';
Expand All @@ -10,8 +15,17 @@ import * as v from 'valibot';
import type { PageServerLoad } from './$types';

const bulkProjectOperationSchema = v.object({
operation: v.nullable(v.picklist(['archive', 'reactivate'])),
projects: v.array(v.object({ Id: idSchema, OwnerId: idSchema, Archived: v.boolean() }))
operation: v.nullable(v.picklist(['archive', 'reactivate', 'claim', 'rebuild'])),
projects: v.array(
v.object({
Id: idSchema,
OwnerId: idSchema,
GroupId: idSchema,
DateArchived: v.nullable(v.date())
})
),
// used to distinguish between single and bulk. will be null if bulk
singleId: v.nullable(idSchema)
});

function whereStatements(
Expand Down Expand Up @@ -91,7 +105,8 @@ export const load = (async ({ params, url, locals }) => {
productDefinitions: await prisma.productDefinitions.findMany(),
actionForm: await superValidate(valibot(bulkProjectOperationSchema)),
allowArchive: params.filter !== 'archived',
allowReactivate: params.filter === 'all' || params.filter === 'archived'
allowReactivate: params.filter === 'all' || params.filter === 'archived',
userGroups: (await userGroupsForOrg(userId!, orgId)).map((g) => g.GroupId)
};
}) satisfies PageServerLoad;

Expand Down Expand Up @@ -128,20 +143,23 @@ export const actions: Actions = {

return { form, ok: true, query: { data: pruneProjects(projects), count } };
},
archive: async (event) => {
projectAction: async (event) => {
const session = await event.locals.auth();
if (!session) return fail(403);
const orgId = parseInt(event.params.id!);
if (isNaN(orgId) || !(orgId + '' === event.params.id)) return fail(404);

const form = await superValidate(event.request, valibot(bulkProjectOperationSchema));
console.log(JSON.stringify(form, null, 4));
if (!form.valid || !form.data.operation) return fail(400, { form, ok: false });
if (
!form.data.projects.reduce((p, c) => p && canModifyProject(session, c.OwnerId, orgId), true)
) {
if (!form.data.projects.every((p) => canModifyProject(session, p.OwnerId, orgId))) {
return fail(403);
}

const groups =
form.data.operation === 'claim'
? (await userGroupsForOrg(session.user.userId, orgId)).map((g) => g.GroupId)
: [];
await Promise.all(
form.data.projects.map(async ({ Id }) => {
const project = await prisma.projects.findUnique({
Expand All @@ -150,33 +168,32 @@ export const actions: Actions = {
},
select: {
Id: true,
DateArchived: true
DateArchived: true,
OwnerId: true,
GroupId: true
}
});
if (form.data.operation === 'archive' && !project?.DateArchived) {
await DatabaseWrites.projects.update(Id, {
DateArchived: new Date()
});
/*await Queues.UserTasks.add(`Delete UserTasks for Archived Project #${Id}`, {
type: BullMQ.JobType.UserTasks_Modify,
scope: 'Project',
projectId: Id,
operation: {
type: BullMQ.UserTasks.OpType.Delete
}
});*/
} else if (form.data.operation === 'reactivate' && !!project?.DateArchived) {
await DatabaseWrites.projects.update(Id, {
DateArchived: null
});
/*await Queues.UserTasks.add(`Create UserTasks for Reactivated Project #${Id}`, {
type: BullMQ.JobType.UserTasks_Modify,
scope: 'Project',
projectId: Id,
operation: {
type: BullMQ.UserTasks.OpType.Create
}
});*/
if (project) {
if (form.data.operation === 'archive' && !project?.DateArchived) {
await DatabaseWrites.projects.update(Id, {
DateArchived: new Date()
});
// TODO: Delete UserTasks for Archived Project?
} else if (form.data.operation === 'reactivate' && !!project?.DateArchived) {
await DatabaseWrites.projects.update(Id, {
DateArchived: null
});
// TODO: Create UserTasks for Reactivated Project?
} else if (
form.data.operation === 'claim' &&
canClaimProject(session, project?.OwnerId, orgId, project?.GroupId, groups)
) {
await DatabaseWrites.projects.update(Id, {
OwnerId: session.user.userId
});
} else if (form.data.operation === 'rebuild') {
console.log('Rebuild not implemented');
}
}
})
);
Expand Down
Loading

0 comments on commit 0ab2b2e

Please sign in to comment.