add + remove users from organisation

This commit is contained in:
Oliver Bryan
2026-01-01 10:46:54 +00:00
parent 8511c6472c
commit 63fef4a0e9
20 changed files with 560 additions and 32 deletions

View File

@@ -1,4 +1,4 @@
import { Organisation, OrganisationMember } from "@issue/shared";
import { Organisation, OrganisationMember, User } from "@issue/shared";
import { and, eq } from "drizzle-orm";
import { db } from "../client";
@@ -103,10 +103,21 @@ export async function getOrganisationMembers(organisationId: number) {
.select()
.from(OrganisationMember)
.where(eq(OrganisationMember.organisationId, organisationId))
.innerJoin(Organisation, eq(OrganisationMember.organisationId, Organisation.id));
.innerJoin(Organisation, eq(OrganisationMember.organisationId, Organisation.id))
.innerJoin(User, eq(OrganisationMember.userId, User.id));
return members;
}
export async function getOrganisationMemberRole(organisationId: number, userId: number) {
const [member] = await db
.select()
.from(OrganisationMember)
.where(
and(eq(OrganisationMember.organisationId, organisationId), eq(OrganisationMember.userId, userId)),
);
return member;
}
export async function removeOrganisationMember(organisationId: number, userId: number) {
await db
.delete(OrganisationMember)

View File

@@ -18,6 +18,7 @@ const main = async () => {
"/auth/login": withCors(routes.authLogin),
"/auth/me": withCors(withAuth(routes.authMe)),
"/user/by-username": withCors(withAuth(routes.userByUsername)),
"/user/update": withCors(withAuth(routes.userUpdate)),
"/user/upload-avatar": withCors(routes.userUploadAvatar),

View File

@@ -23,6 +23,7 @@ import projectDelete from "./project/delete";
import projectUpdate from "./project/update";
import projectWithCreator from "./project/with-creator";
import projectsWithCreators from "./project/with-creators";
import userByUsername from "./user/by-username";
import userUpdate from "./user/update";
import userUploadAvatar from "./user/upload-avatar";
@@ -31,6 +32,7 @@ export const routes = {
authLogin,
authMe,
userByUsername,
userUpdate,
userUploadAvatar,

View File

@@ -1,8 +1,13 @@
import type { BunRequest } from "bun";
import { createOrganisationMember, getOrganisationById, getUserById } from "../../db/queries";
import type { AuthedRequest } from "../../auth/middleware";
import {
createOrganisationMember,
getOrganisationById,
getOrganisationMemberRole,
getUserById,
} from "../../db/queries";
// /organisation/add-member?organisationId=1&userId=2&role=member
export default async function organisationAddMember(req: BunRequest) {
export default async function organisationAddMember(req: AuthedRequest) {
const url = new URL(req.url);
const organisationId = url.searchParams.get("organisationId");
const userId = url.searchParams.get("userId");
@@ -32,6 +37,20 @@ export default async function organisationAddMember(req: BunRequest) {
return new Response(`user with id ${userId} not found`, { status: 404 });
}
const existingMember = await getOrganisationMemberRole(orgIdNumber, userIdNumber);
if (existingMember) {
return new Response("User is already a member of this organisation", { status: 409 });
}
const requesterMember = await getOrganisationMemberRole(orgIdNumber, req.userId);
if (!requesterMember) {
return new Response("You are not a member of this organisation", { status: 403 });
}
if (requesterMember.role !== "owner" && requesterMember.role !== "admin") {
return new Response("Only owners and admins can add members", { status: 403 });
}
const member = await createOrganisationMember(orgIdNumber, userIdNumber, role);
return Response.json(member);

View File

@@ -1,8 +1,8 @@
import type { BunRequest } from "bun";
import { getOrganisationById, getUserById, removeOrganisationMember } from "../../db/queries";
import type { AuthedRequest } from "../../auth/middleware";
import { getOrganisationById, getOrganisationMemberRole, removeOrganisationMember } from "../../db/queries";
// /organisation/remove-member?organisationId=1&userId=2
export default async function organisationRemoveMember(req: BunRequest) {
export default async function organisationRemoveMember(req: AuthedRequest) {
const url = new URL(req.url);
const organisationId = url.searchParams.get("organisationId");
const userId = url.searchParams.get("userId");
@@ -26,9 +26,22 @@ export default async function organisationRemoveMember(req: BunRequest) {
return new Response(`organisation with id ${organisationId} not found`, { status: 404 });
}
const user = await getUserById(userIdNumber);
if (!user) {
return new Response(`user with id ${userId} not found`, { status: 404 });
const memberToRemove = await getOrganisationMemberRole(orgIdNumber, userIdNumber);
if (!memberToRemove) {
return new Response("User is not a member of this organisation", { status: 404 });
}
if (memberToRemove.role === "owner") {
return new Response("Cannot remove the organisation owner", { status: 403 });
}
const requesterMember = await getOrganisationMemberRole(orgIdNumber, req.userId);
if (!requesterMember) {
return new Response("You are not a member of this organisation", { status: 403 });
}
if (requesterMember.role !== "owner" && requesterMember.role !== "admin") {
return new Response("Only owners and admins can remove members", { status: 403 });
}
await removeOrganisationMember(orgIdNumber, userIdNumber);

View File

@@ -0,0 +1,19 @@
import type { BunRequest } from "bun";
import { getUserByUsername } from "../../db/queries";
// /user/by-username?username=someusername
export default async function userByUsername(req: BunRequest) {
const url = new URL(req.url);
const username = url.searchParams.get("username");
if (!username) {
return new Response("username is required", { status: 400 });
}
const user = await getUserByUsername(username);
if (!user) {
return new Response(`User with username '${username}' not found`, { status: 404 });
}
return Response.json(user);
}