Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

updates #132

Merged
merged 11 commits into from
Jun 11, 2024
2 changes: 1 addition & 1 deletion backend/src/lib/common-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ export const colorSchema = z
.max(7)
.regex(/^#(?:[0-9a-fA-F]{3}){1,2}$/, 'Color may only contain letters, numbers & starts with #');

export const validUrlSchema = z.string().refine((url: string) => url.startsWith('https'), 'URL must start with https://');
export const validUrlSchema = z.string().refine((url: string) => url.startsWith('https'), 'URL must start with https://');
4 changes: 2 additions & 2 deletions backend/src/middlewares/guard/is-allowed-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async function createEntityContext(entityType: string, ctx: any) {
if (!entity) return;

// Extract payload from request body
const payload = ctx.req.valid('json');
const payload = await ctx.req.json();

// Initialize context to store the custom created entity context based on the lowest possible ancestor
const context: Record<string, string> = { entity: entityType.toUpperCase() };
Expand All @@ -110,7 +110,7 @@ async function createEntityContext(entityType: string, ctx: any) {

// If not found in params or query, check if it's provided in the request body
if (!lowestAncestorIdOrSlug && payload) {
lowestAncestorIdOrSlug = payload[ancestor.name];
lowestAncestorIdOrSlug = payload[`${ancestor.name}Id`];
}

// If identifier is found, resolve the lowest ancestor
Expand Down
15 changes: 0 additions & 15 deletions backend/src/modules/general/helpers/check-role.ts

This file was deleted.

6 changes: 0 additions & 6 deletions backend/src/modules/general/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import { getOrderColumn } from '../../lib/order-column';
import { isAuthenticated } from '../../middlewares/guard';
import { logEvent } from '../../middlewares/logger/log-event';
import { CustomHono } from '../../types/common';
import { apiUserSchema } from '../users/schema';
import { checkRole } from './helpers/check-role';
import { checkSlugAvailable } from './helpers/check-slug';
import {
acceptInviteRouteConfig,
Expand Down Expand Up @@ -155,10 +153,6 @@ const generalRoutes = app
const { emails, role } = ctx.req.valid('json');
const user = ctx.get('user');

if (user.role !== 'ADMIN') return errorResponse(ctx, 403, 'forbidden', 'warn');

if (!checkRole(apiUserSchema, role)) return errorResponse(ctx, 400, 'invalid_role', 'warn');

for (const email of emails) {
const [targetUser] = (await db.select().from(usersTable).where(eq(usersTable.email, email.toLowerCase()))) as (User | undefined)[];

Expand Down
17 changes: 17 additions & 0 deletions backend/src/modules/memberships/helpers/to-membership-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { MembershipModel } from "../../../db/schema/memberships"
import type { membershipInfoType } from "../schema";

/**
* Converts a membership to a membershipInfo object.
*
* @param {MembershipModel | undefined | null} membership - The membership to be converted. (Can also be undefined or null).
* @returns {membershipInfoType | null} The converted membership information object, or null if the input is undefined or null.
*/
export const toMembershipInfo = (membership: MembershipModel | undefined | null): membershipInfoType | null => {
return membership ? {
id: membership.id,
createdAt: membership.createdAt.toString(),
role: membership.role,
archived: membership.inactive || false,
} : null;
}
9 changes: 1 addition & 8 deletions backend/src/modules/memberships/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ import permissionManager from '../../lib/permission-manager';
import { sendSSEToUsers } from '../../lib/sse';
import { logEvent } from '../../middlewares/logger/log-event';
import { CustomHono } from '../../types/common';
import { checkRole } from '../general/helpers/check-role';
import { createMembershipRouteConfig, deleteMembershipsRouteConfig, updateMembershipRouteConfig } from './routes';
import { apiMembershipSchema } from './schema';

const app = new CustomHono();

Expand All @@ -29,7 +27,7 @@ const membershipsRoutes = app
/*
* Invite members to an entity such as an organization
*/
// TODO: make this work for Projects too
// TODO: make this work for Projects too and check how this endpoint is protected from unauthorized access
.openapi(createMembershipRouteConfig, async (ctx) => {
const { idOrSlug } = ctx.req.valid('query');
const { emails, role } = ctx.req.valid('json');
Expand All @@ -40,11 +38,6 @@ const membershipsRoutes = app

if (!organization) return errorResponse(ctx, 403, 'forbidden', 'warn');

// Check to invite on organization level
if (organization && !checkRole(apiMembershipSchema, role)) {
return errorResponse(ctx, 400, 'invalid_role', 'warn');
}

for (const email of emails) {
const [targetUser] = (await db.select().from(usersTable).where(eq(usersTable.email, email.toLowerCase()))) as (User | undefined)[];

Expand Down
9 changes: 9 additions & 0 deletions backend/src/modules/memberships/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,12 @@ export const deleteMembersQuerySchema = z.object({
entityType: z.enum(config.contextEntityTypes),
ids: z.union([z.string(), z.array(z.string())]),
});

export const membershipInfoSchema = z.object({
id: apiMembershipSchema.shape.id,
role: apiMembershipSchema.shape.role,
createdAt: apiMembershipSchema.shape.createdAt,
archived: apiMembershipSchema.shape.inactive,
}).nullable();

export type membershipInfoType = z.infer<typeof membershipInfoSchema>;
33 changes: 17 additions & 16 deletions backend/src/modules/organizations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getOrganizationsRouteConfig,
updateOrganizationRouteConfig,
} from './routes';
import { toMembershipInfo } from '../memberships/helpers/to-membership-info';

const app = new CustomHono();

Expand Down Expand Up @@ -49,12 +50,15 @@ const organizationsRoutes = app

logEvent('Organization created', { organization: createdOrganization.id });

await db.insert(membershipsTable).values({
const [createdMembership] = await db
.insert(membershipsTable)
.values({
type: 'ORGANIZATION',
userId: user.id,
organizationId: createdOrganization.id,
role: 'ADMIN',
});
})
.returning();

logEvent('User added to organization', { user: user.id, organization: createdOrganization.id });

Expand All @@ -65,7 +69,7 @@ const organizationsRoutes = app
success: true,
data: {
...createdOrganization,
userRole: 'ADMIN' as const,
membership: toMembershipInfo(createdMembership),
counts: {
admins: 1,
members: 1,
Expand Down Expand Up @@ -99,21 +103,18 @@ const organizationsRoutes = app
.groupBy(membershipsTable.organizationId)
.as('counts');

const membershipRoles = db
.select({
organizationId: membershipsTable.organizationId,
role: membershipsTable.role,
})
const memberships = db
.select()
.from(membershipsTable)
.where(and(eq(membershipsTable.userId, user.id), eq(membershipsTable.type, 'ORGANIZATION')))
.as('membership_roles');
.as('memberships');

const orderColumn = getOrderColumn(
{
id: organizationsTable.id,
name: organizationsTable.name,
createdAt: organizationsTable.createdAt,
userRole: membershipRoles.role,
userRole: memberships.role,
},
sort,
organizationsTable.id,
Expand All @@ -123,12 +124,12 @@ const organizationsRoutes = app
const organizations = await db
.select({
organization: organizationsTable,
userRole: membershipRoles.role,
membership: membershipsTable,
admins: counts.admins,
members: counts.members,
})
.from(organizationsQuery.as('organizations'))
.leftJoin(membershipRoles, eq(organizationsTable.id, membershipRoles.organizationId))
.leftJoin(memberships, eq(organizationsTable.id, memberships.organizationId))
.leftJoin(counts, eq(organizationsTable.id, counts.organizationId))
.orderBy(orderColumn)
.limit(Number(limit))
Expand All @@ -138,9 +139,9 @@ const organizationsRoutes = app
{
success: true,
data: {
items: organizations.map(({ organization, userRole, admins, members }) => ({
items: organizations.map(({ organization, membership, admins, members }) => ({
...organization,
userRole,
membership: toMembershipInfo(membership),
counts: { admins, members },
})),
total,
Expand Down Expand Up @@ -241,7 +242,7 @@ const organizationsRoutes = app
success: true,
data: {
...updatedOrganization,
userRole: memberships.find((member) => member.id === user.id)?.role || null,
membership: toMembershipInfo(memberships.find((member) => member.id === user.id)),
counts: {
admins,
members,
Expand Down Expand Up @@ -282,7 +283,7 @@ const organizationsRoutes = app
success: true,
data: {
...organization,
userRole: membership?.role || null,
membership: toMembershipInfo(membership),
counts: {
admins,
members,
Expand Down
4 changes: 2 additions & 2 deletions backend/src/modules/organizations/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
validSlugSchema,
validUrlSchema,
} from '../../lib/common-schemas';
import { apiMembershipSchema } from '../memberships/schema';
import { membershipInfoSchema } from '../memberships/schema';

export const apiOrganizationSchema = z.object({
...createSelectSchema(organizationsTable).shape,
Expand All @@ -19,7 +19,7 @@ export const apiOrganizationSchema = z.object({
languages: z.array(z.string()),
emailDomains: z.array(z.string()).nullable(),
authStrategies: z.array(z.string()).nullable(),
userRole: apiMembershipSchema.shape.role.nullable(),
membership: membershipInfoSchema,
counts: z.object({
admins: z.number(),
members: z.number(),
Expand Down
45 changes: 21 additions & 24 deletions backend/src/modules/projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getProjectsRouteConfig,
updateProjectRouteConfig,
} from './routes';
import { toMembershipInfo } from '../memberships/helpers/to-membership-info';

const app = new CustomHono();

Expand Down Expand Up @@ -48,13 +49,15 @@ const projectsRoutes = app

logEvent('Project created', { project: project.id });

await db.insert(membershipsTable).values({
const [createdMembership] = await db
.insert(membershipsTable)
.values({
userId: user.id,
organizationId,
projectId: project.id,
type: 'PROJECT',
role: 'ADMIN',
});
}).returning();

logEvent('User added to project', { user: user.id, project: project.id });

Expand All @@ -68,7 +71,7 @@ const projectsRoutes = app
logEvent('Project added to workspace', { project: project.id, workspace: workspaceId });
}

const createdProject = { ...project, counts: { admins: 1, members: 0 }, role: 'ADMIN' as const };
const createdProject = { ...project, counts: { admins: 1, members: 0 }, membership: toMembershipInfo(createdMembership) };

sendSSE(user.id, 'create_entity', createdProject);

Expand All @@ -78,21 +81,17 @@ const projectsRoutes = app
* Get project by id or slug
*/
.openapi(getProjectRouteConfig, async (ctx) => {
const user = ctx.get('user');
const project = ctx.get('project');

const [membership] = await db
.select()
.from(membershipsTable)
.where(and(eq(membershipsTable.userId, user.id), eq(membershipsTable.projectId, project.id)));
const memberships = ctx.get('memberships');
const membership = memberships.find(m => m.projectId === project.id && m.type === 'PROJECT')

// TODO fix counts using a helper
return ctx.json(
{
success: true,
data: {
...project,
role: membership?.role || null,
membership: toMembershipInfo(membership),
counts: { admins: 0, members: 0 },
},
},
Expand Down Expand Up @@ -125,21 +124,19 @@ const projectsRoutes = app
.groupBy(membershipsTable.projectId)
.as('counts');

const membership = db
.select({
projectId: membershipsTable.projectId,
role: membershipsTable.role,
})
// @TODO: Permission check which projects a user is allowed to see? (this will skip when requestedUserId is used in query!)
const memberships = db
.select()
.from(membershipsTable)
.where(eq(membershipsTable.userId, requestedUserId ? requestedUserId : user.id))
.as('membership_roles');
.as('memberships');

const orderColumn = getOrderColumn(
{
id: projectsTable.id,
name: projectsTable.name,
createdAt: projectsTable.createdAt,
userRole: membership.role,
userRole: memberships.role,
},
sort,
projectsTable.id,
Expand All @@ -153,13 +150,13 @@ const projectsRoutes = app
projects = await db
.select({
project: projectsTable,
role: membership.role,
membership: membershipsTable,
workspaceId: projectsToWorkspacesTable.workspaceId,
admins: counts.admins,
members: counts.members,
})
.from(projectsQuery.as('projects'))
.innerJoin(membership, eq(membership.projectId, projectsTable.id))
.innerJoin(memberships, eq(memberships.projectId, projectsTable.id))
.leftJoin(projectsToWorkspacesTable, eq(projectsToWorkspacesTable.projectId, projectsTable.id))
.leftJoin(counts, eq(projectsTable.id, counts.projectId))
.orderBy(orderColumn)
Expand All @@ -169,7 +166,7 @@ const projectsRoutes = app
projects = await db
.select({
project: projectsTable,
role: membership.role,
membership: membershipsTable,
workspaceId: projectsToWorkspacesTable.workspaceId,
admins: counts.admins,
members: counts.members,
Expand All @@ -180,7 +177,7 @@ const projectsRoutes = app
and(eq(projectsToWorkspacesTable.projectId, projectsTable.id), eq(projectsToWorkspacesTable.workspaceId, workspaceId), ...projectsFilters),
)
.leftJoin(counts, eq(projectsTable.id, counts.projectId))
.leftJoin(membership, and(eq(membership.projectId, projectsTable.id)))
.leftJoin(memberships, and(eq(memberships.projectId, projectsTable.id)))
.where(eq(projectsToWorkspacesTable.workspaceId, workspaceId))
.orderBy(orderColumn)
.limit(Number(limit))
Expand All @@ -191,9 +188,9 @@ const projectsRoutes = app
{
success: true,
data: {
items: projects.map(({ project, role, workspaceId, admins, members }) => ({
items: projects.map(({ project, membership, workspaceId, admins, members }) => ({
...project,
role,
membership: toMembershipInfo(membership),
workspaceId,
counts: { admins, members },
})),
Expand Down Expand Up @@ -250,7 +247,7 @@ const projectsRoutes = app
success: true,
data: {
...updatedProject,
role: memberships.find((member) => member.id === user.id)?.role || null,
membership: toMembershipInfo(memberships.find((member) => member.id === user.id)),
counts: { admins: 0, members: 0 },
},
},
Expand Down
Loading