feat: scope organization access by memberships

This commit is contained in:
2026-06-24 18:04:43 +01:00
parent 92f4e54cf4
commit f0ec055725
14 changed files with 299 additions and 126 deletions
@@ -5,9 +5,9 @@
type Organization = { type Organization = {
name: string; name: string;
workspace: string;
addressLine1?: string | null; addressLine1?: string | null;
addressLine2?: string | null; addressLine2?: string | null;
addressLine3?: string | null;
city?: string | null; city?: string | null;
region?: string | null; region?: string | null;
postcode?: string | null; postcode?: string | null;
@@ -43,26 +43,17 @@
/> />
</Field.Field> </Field.Field>
<Field.Field> <Field.Field>
<Field.Label for={`${prefix}-workspace`}>Workspace</Field.Label> <Field.Label for={`${prefix}-address-line-1`}>Address line 1</Field.Label>
<Input <Input
id={`${prefix}-workspace`} id={`${prefix}-address-line-1`}
name="workspace" name="addressLine1"
type="text" type="text"
value={organization?.workspace ?? 'Blueprint Workspace'} value={organization?.addressLine1 ?? ''}
required required
/> />
</Field.Field> </Field.Field>
</div> </div>
<Field.Field>
<Field.Label for={`${prefix}-address-line-1`}>Address</Field.Label>
<Input
id={`${prefix}-address-line-1`}
name="addressLine1"
type="text"
value={organization?.addressLine1 ?? ''}
/>
</Field.Field>
<Field.Field> <Field.Field>
<Field.Label for={`${prefix}-address-line-2`}>Address line 2</Field.Label> <Field.Label for={`${prefix}-address-line-2`}>Address line 2</Field.Label>
<Input <Input
@@ -72,6 +63,15 @@
value={organization?.addressLine2 ?? ''} value={organization?.addressLine2 ?? ''}
/> />
</Field.Field> </Field.Field>
<Field.Field>
<Field.Label for={`${prefix}-address-line-3`}>Address line 3</Field.Label>
<Input
id={`${prefix}-address-line-3`}
name="addressLine3"
type="text"
value={organization?.addressLine3 ?? ''}
/>
</Field.Field>
<div class="grid gap-2 sm:grid-cols-2"> <div class="grid gap-2 sm:grid-cols-2">
<Field.Field> <Field.Field>
<Field.Label for={`${prefix}-city`}>City</Field.Label> <Field.Label for={`${prefix}-city`}>City</Field.Label>
@@ -15,7 +15,8 @@
type Organization = { type Organization = {
id: string; id: string;
name: string; name: string;
workspace: string; addressLine1?: string | null;
membershipRole?: string;
}; };
let { let {
@@ -48,7 +49,9 @@
</div> </div>
<div class="grid flex-1 text-left text-sm leading-tight"> <div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{activeOrganization.name}</span> <span class="truncate font-semibold">{activeOrganization.name}</span>
<span class="truncate text-xs">{activeOrganization.workspace}</span> <span class="truncate text-xs"
>{activeOrganization.addressLine1 || 'Address not set'}</span
>
</div> </div>
<ChevronsUpDownIcon class="ms-auto" /> <ChevronsUpDownIcon class="ms-auto" />
</Sidebar.MenuButton> </Sidebar.MenuButton>
@@ -73,7 +76,7 @@
<div class="grid flex-1 text-left text-sm leading-tight"> <div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{organization.name}</span> <span class="truncate font-semibold">{organization.name}</span>
<span class="truncate text-xs text-muted-foreground" <span class="truncate text-xs text-muted-foreground"
>{organization.workspace}</span >{organization.addressLine1 || 'Address not set'}</span
> >
</div> </div>
{#if organization.id === activeOrganization.id} {#if organization.id === activeOrganization.id}
@@ -116,7 +119,9 @@
</div> </div>
<div class="grid flex-1 text-left text-sm leading-tight"> <div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">Create organization</span> <span class="truncate font-semibold">Create organization</span>
<span class="truncate text-xs text-muted-foreground">Add a new workspace</span> <span class="truncate text-xs text-muted-foreground"
>Add address and payment details</span
>
</div> </div>
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
+1 -1
View File
@@ -9,7 +9,7 @@ const roleSchema = z.enum(['admin', 'user'], 'Select a role.');
export const createAdminSchema = z.object({ export const createAdminSchema = z.object({
name: z.string().trim().min(1, 'Enter a name.'), name: z.string().trim().min(1, 'Enter a name.'),
email: z.email('Enter a valid email address.').transform((value) => value.toLowerCase()), email: z.email('Enter a valid email address.').transform((value) => value.toLowerCase()),
password: z.string().min(8, passwordMessage), password: optionalPassword,
role: roleSchema.default('admin') role: roleSchema.default('admin')
}); });
+80 -37
View File
@@ -1,8 +1,7 @@
import { asc, eq, isNull } from 'drizzle-orm'; import { error } from '@sveltejs/kit';
import { and, asc, eq, isNull } from 'drizzle-orm';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { organizations, session } from '$lib/server/db/schema'; import { organizationMemberships, organizations, session } from '$lib/server/db/schema';
const DEFAULT_ORGANIZATION_ID = 'default-organization';
type SessionWithOrganization = { type SessionWithOrganization = {
id: string; id: string;
@@ -10,43 +9,46 @@ type SessionWithOrganization = {
}; };
export async function loadOrganizationContext(locals: App.Locals) { export async function loadOrganizationContext(locals: App.Locals) {
if (!locals.user) {
error(401, 'Sign in before opening the dashboard.');
}
const rows = await db const rows = await db
.select() .select({
id: organizations.id,
name: organizations.name,
addressLine1: organizations.addressLine1,
addressLine2: organizations.addressLine2,
addressLine3: organizations.addressLine3,
city: organizations.city,
region: organizations.region,
postcode: organizations.postcode,
country: organizations.country,
bankName: organizations.bankName,
bankAccountName: organizations.bankAccountName,
bankAccountNumber: organizations.bankAccountNumber,
bankSortCode: organizations.bankSortCode,
bankIban: organizations.bankIban,
bankSwift: organizations.bankSwift,
createdAt: organizations.createdAt,
updatedAt: organizations.updatedAt,
archivedAt: organizations.archivedAt,
membershipRole: organizationMemberships.role
})
.from(organizations) .from(organizations)
.innerJoin(
organizationMemberships,
and(
eq(organizationMemberships.organizationId, organizations.id),
eq(organizationMemberships.userId, locals.user.id),
isNull(organizationMemberships.archivedAt)
)
)
.where(isNull(organizations.archivedAt)) .where(isNull(organizations.archivedAt))
.orderBy(asc(organizations.name)); .orderBy(asc(organizations.name));
if (rows.length === 0) { if (rows.length === 0) {
const now = new Date(); error(403, 'You have not been added to an organization.');
await db.insert(organizations).values({
id: DEFAULT_ORGANIZATION_ID,
name: 'Steel City Stadium',
workspace: 'Blueprint Workspace',
createdAt: now,
updatedAt: now
});
rows.push({
id: DEFAULT_ORGANIZATION_ID,
name: 'Steel City Stadium',
workspace: 'Blueprint Workspace',
addressLine1: null,
addressLine2: null,
city: null,
region: null,
postcode: null,
country: 'United Kingdom',
bankName: null,
bankAccountName: null,
bankAccountNumber: null,
bankSortCode: null,
bankIban: null,
bankSwift: null,
createdAt: now,
updatedAt: now,
archivedAt: null
});
} }
const currentSession = locals.session as SessionWithOrganization | undefined; const currentSession = locals.session as SessionWithOrganization | undefined;
@@ -66,14 +68,45 @@ export async function loadOrganizationContext(locals: App.Locals) {
return { return {
organizations: rows, organizations: rows,
activeOrganization, activeOrganization,
activeOrganizationId: activeOrganization.id activeOrganizationId: activeOrganization.id,
activeMembershipRole: activeOrganization.membershipRole
}; };
} }
export async function setActiveOrganization(locals: App.Locals, organizationId: string) { export async function setActiveOrganization(locals: App.Locals, organizationId: string) {
if (!locals.user) return null;
const [organization] = await db const [organization] = await db
.select() .select({
id: organizations.id,
name: organizations.name,
addressLine1: organizations.addressLine1,
addressLine2: organizations.addressLine2,
addressLine3: organizations.addressLine3,
city: organizations.city,
region: organizations.region,
postcode: organizations.postcode,
country: organizations.country,
bankName: organizations.bankName,
bankAccountName: organizations.bankAccountName,
bankAccountNumber: organizations.bankAccountNumber,
bankSortCode: organizations.bankSortCode,
bankIban: organizations.bankIban,
bankSwift: organizations.bankSwift,
createdAt: organizations.createdAt,
updatedAt: organizations.updatedAt,
archivedAt: organizations.archivedAt,
membershipRole: organizationMemberships.role
})
.from(organizations) .from(organizations)
.innerJoin(
organizationMemberships,
and(
eq(organizationMemberships.organizationId, organizations.id),
eq(organizationMemberships.userId, locals.user.id),
isNull(organizationMemberships.archivedAt)
)
)
.where(eq(organizations.id, organizationId)) .where(eq(organizations.id, organizationId))
.limit(1); .limit(1);
@@ -92,3 +125,13 @@ export async function setActiveOrganization(locals: App.Locals, organizationId:
return organization; return organization;
} }
export async function requireActiveOrganizationAdmin(locals: App.Locals) {
const context = await loadOrganizationContext(locals);
if (context.activeMembershipRole !== 'admin') {
error(403, 'You must be an organization admin to manage these settings.');
}
return context;
}
@@ -14,8 +14,6 @@
children: import('svelte').Snippet; children: import('svelte').Snippet;
} = $props(); } = $props();
let editOpen = $state(false);
const tabs = [ const tabs = [
{ value: 'details', label: 'Details' }, { value: 'details', label: 'Details' },
{ value: 'admins', label: 'Admins' } { value: 'admins', label: 'Admins' }
@@ -46,7 +44,7 @@
Manage organization details, invoice payment information, and administrators. Manage organization details, invoice payment information, and administrators.
</p> </p>
</div> </div>
<OrganizationEditDialog organization={data.activeOrganization} bind:open={editOpen} /> <OrganizationEditDialog organization={data.activeOrganization} />
</div> </div>
</div> </div>
@@ -1,25 +1,34 @@
import { asc, eq } from 'drizzle-orm'; import { and, asc, count, eq, isNull } from 'drizzle-orm';
import { message, superValidate } from 'sveltekit-superforms/server'; import { message, superValidate } from 'sveltekit-superforms/server';
import { zod4 } from 'sveltekit-superforms/adapters'; import { zod4 } from 'sveltekit-superforms/adapters';
import { auth } from '$lib/server/auth'; import { auth } from '$lib/server/auth';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { user } from '$lib/server/db/schema'; import { organizationMemberships, user } from '$lib/server/db/schema';
import { requireActiveOrganizationAdmin } from '$lib/server/organizations';
import { createAdminSchema, deleteAdminSchema, editAdminSchema } from '$lib/schemas/admin'; import { createAdminSchema, deleteAdminSchema, editAdminSchema } from '$lib/schemas/admin';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => { export const load: PageServerLoad = async ({ locals }) => {
const { activeOrganizationId } = await requireActiveOrganizationAdmin(locals);
const users = await db const users = await db
.select({ .select({
id: user.id, id: user.id,
name: user.name, name: user.name,
email: user.email, email: user.email,
role: user.role, role: organizationMemberships.role,
banned: user.banned, banned: user.banned,
emailVerified: user.emailVerified, emailVerified: user.emailVerified,
createdAt: user.createdAt, createdAt: user.createdAt,
updatedAt: user.updatedAt updatedAt: user.updatedAt
}) })
.from(user) .from(organizationMemberships)
.innerJoin(user, eq(organizationMemberships.userId, user.id))
.where(
and(
eq(organizationMemberships.organizationId, activeOrganizationId),
isNull(organizationMemberships.archivedAt)
)
)
.orderBy(asc(user.name), asc(user.email)); .orderBy(asc(user.name), asc(user.email));
return { return {
@@ -37,8 +46,24 @@ export const load: PageServerLoad = async ({ locals }) => {
}; };
}; };
async function countOrganizationAdmins(organizationId: string) {
const [result] = await db
.select({ total: count() })
.from(organizationMemberships)
.where(
and(
eq(organizationMemberships.organizationId, organizationId),
eq(organizationMemberships.role, 'admin'),
isNull(organizationMemberships.archivedAt)
)
);
return result.total;
}
export const actions: Actions = { export const actions: Actions = {
create: async (event) => { create: async (event) => {
const { activeOrganizationId } = await requireActiveOrganizationAdmin(event.locals);
const form = await superValidate(event, zod4(createAdminSchema), { id: 'create-admin' }); const form = await superValidate(event, zod4(createAdminSchema), { id: 'create-admin' });
if (!form.valid) { if (!form.valid) {
@@ -46,34 +71,108 @@ export const actions: Actions = {
} }
try { try {
await auth.api.createUser({ const [existingUser] = await db
headers: event.request.headers, .select({ id: user.id })
body: { .from(user)
name: form.data.name, .where(eq(user.email, form.data.email))
email: form.data.email, .limit(1);
password: form.data.password,
role: form.data.role let userId = existingUser?.id;
if (!userId) {
if (!form.data.password) {
return message(form, 'Enter a password when creating a new user.', { status: 400 });
} }
await auth.api.createUser({
body: {
name: form.data.name,
email: form.data.email,
password: form.data.password,
role: form.data.role
}
});
const [createdUser] = await db
.select({ id: user.id })
.from(user)
.where(eq(user.email, form.data.email))
.limit(1);
userId = createdUser?.id;
}
if (!userId) {
return message(form, 'Unable to create user.', { status: 400 });
}
const [existingMembership] = await db
.select({ id: organizationMemberships.id })
.from(organizationMemberships)
.where(
and(
eq(organizationMemberships.organizationId, activeOrganizationId),
eq(organizationMemberships.userId, userId),
isNull(organizationMemberships.archivedAt)
)
)
.limit(1);
if (existingMembership) {
return message(form, 'That user already has access to this organization.', {
status: 409
});
}
await db.insert(organizationMemberships).values({
organizationId: activeOrganizationId,
userId,
role: form.data.role,
createdAt: new Date(),
updatedAt: new Date()
}); });
} catch { } catch {
return message(form, 'Unable to create admin. Check whether that email already exists.', { return message(form, 'Unable to add user. Check whether that email is valid.', {
status: 400 status: 400
}); });
} }
return message(form, 'Admin created.'); return message(form, 'User added to this organization.');
}, },
edit: async (event) => { edit: async (event) => {
const { activeOrganizationId } = await requireActiveOrganizationAdmin(event.locals);
const form = await superValidate(event, zod4(editAdminSchema), { id: 'edit-admin' }); const form = await superValidate(event, zod4(editAdminSchema), { id: 'edit-admin' });
if (!form.valid) { if (!form.valid) {
return message(form, 'Check the highlighted fields.', { status: 400 }); return message(form, 'Check the highlighted fields.', { status: 400 });
} }
const [membership] = await db
.select({ id: organizationMemberships.id, role: organizationMemberships.role })
.from(organizationMemberships)
.where(
and(
eq(organizationMemberships.organizationId, activeOrganizationId),
eq(organizationMemberships.userId, form.data.id),
isNull(organizationMemberships.archivedAt)
)
)
.limit(1);
if (!membership) {
return message(form, 'That user does not belong to this organization.', { status: 404 });
}
if (membership.role === 'admin' && form.data.role !== 'admin') {
const adminCount = await countOrganizationAdmins(activeOrganizationId);
if (adminCount <= 1) {
return message(form, 'Each organization must keep at least one admin.', { status: 400 });
}
}
try { try {
await auth.api.adminUpdateUser({ await auth.api.adminUpdateUser({
headers: event.request.headers,
body: { body: {
userId: form.data.id, userId: form.data.id,
data: { data: {
@@ -83,41 +182,61 @@ export const actions: Actions = {
} }
}); });
await auth.api.setRole({
headers: event.request.headers,
body: {
userId: form.data.id,
role: form.data.role
}
});
if (form.data.password) { if (form.data.password) {
await auth.api.setUserPassword({ await auth.api.setUserPassword({
headers: event.request.headers,
body: { body: {
userId: form.data.id, userId: form.data.id,
newPassword: form.data.password newPassword: form.data.password
} }
}); });
} }
await db
.update(organizationMemberships)
.set({ role: form.data.role, updatedAt: new Date() })
.where(eq(organizationMemberships.id, membership.id));
} catch { } catch {
return message(form, 'Unable to update admin. Check whether that email already exists.', { return message(form, 'Unable to update user. Check whether that email already exists.', {
status: 400 status: 400
}); });
} }
return message(form, 'Admin updated.'); return message(form, 'User updated.');
}, },
delete: async (event) => { delete: async (event) => {
const { activeOrganizationId } = await requireActiveOrganizationAdmin(event.locals);
const form = await superValidate(event, zod4(deleteAdminSchema), { id: 'delete-admin' }); const form = await superValidate(event, zod4(deleteAdminSchema), { id: 'delete-admin' });
if (!form.valid) { if (!form.valid) {
return message(form, 'Admin id is required.', { status: 400 }); return message(form, 'User id is required.', { status: 400 });
} }
await db.delete(user).where(eq(user.id, form.data.id)); const [membership] = await db
.select({ id: organizationMemberships.id, role: organizationMemberships.role })
.from(organizationMemberships)
.where(
and(
eq(organizationMemberships.organizationId, activeOrganizationId),
eq(organizationMemberships.userId, form.data.id),
isNull(organizationMemberships.archivedAt)
)
)
.limit(1);
return message(form, 'Admin deleted.'); if (!membership) {
return message(form, 'That user does not belong to this organization.', { status: 404 });
}
if (membership.role === 'admin') {
const adminCount = await countOrganizationAdmins(activeOrganizationId);
if (adminCount <= 1) {
return message(form, 'Each organization must keep at least one admin.', { status: 400 });
}
}
await db.delete(organizationMemberships).where(eq(organizationMemberships.id, membership.id));
return message(form, 'User removed from this organization.');
} }
}; };
@@ -105,7 +105,7 @@
<div> <div>
<h2 class="text-xl font-semibold tracking-tight">Administrators</h2> <h2 class="text-xl font-semibold tracking-tight">Administrators</h2>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Manage users who can access and administer Clearity. Manage users who can access and administer this organization.
</p> </p>
</div> </div>
@@ -65,7 +65,7 @@
<DropdownMenu.Content align="end" class="w-36"> <DropdownMenu.Content align="end" class="w-36">
<DropdownMenu.Item onclick={() => openEdit(user)}>Edit</DropdownMenu.Item> <DropdownMenu.Item onclick={() => openEdit(user)}>Edit</DropdownMenu.Item>
<DropdownMenu.Item variant="destructive" onclick={() => openDelete(user)}> <DropdownMenu.Item variant="destructive" onclick={() => openDelete(user)}>
Delete Remove access
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
@@ -32,7 +32,9 @@
<Dialog.Content class="sm:max-w-md"> <Dialog.Content class="sm:max-w-md">
<Dialog.Header> <Dialog.Header>
<Dialog.Title>Create user</Dialog.Title> <Dialog.Title>Create user</Dialog.Title>
<Dialog.Description>Add a manually managed Better Auth user.</Dialog.Description> <Dialog.Description>
Create a user or add an existing email to this organization.
</Dialog.Description>
</Dialog.Header> </Dialog.Header>
<form method="POST" action="?/create" class="grid gap-2" use:enhanceCreate> <form method="POST" action="?/create" class="grid gap-2" use:enhanceCreate>
<FormField form={createForm} name="name"> <FormField form={createForm} name="name">
@@ -66,6 +68,7 @@
{...props} {...props}
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
placeholder="Required for new users"
bind:value={$createData.password} bind:value={$createData.password}
/> />
{/snippet} {/snippet}
@@ -17,9 +17,9 @@
<AlertDialog.Root open={!!deletingUser} onOpenChange={(open) => !open && onClose()}> <AlertDialog.Root open={!!deletingUser} onOpenChange={(open) => !open && onClose()}>
<AlertDialog.Content> <AlertDialog.Content>
<AlertDialog.Header> <AlertDialog.Header>
<AlertDialog.Title>Delete user?</AlertDialog.Title> <AlertDialog.Title>Remove access?</AlertDialog.Title>
<AlertDialog.Description> <AlertDialog.Description>
This will permanently delete {deletingUser?.name ?? 'this user'} and revoke their sessions. This will remove {deletingUser?.name ?? 'this user'} from this organization.
</AlertDialog.Description> </AlertDialog.Description>
</AlertDialog.Header> </AlertDialog.Header>
<AlertDialog.Footer> <AlertDialog.Footer>
@@ -27,7 +27,7 @@
{#if deletingUser} {#if deletingUser}
<form method="POST" action="?/delete" use:enhanceDelete> <form method="POST" action="?/delete" use:enhanceDelete>
<input type="hidden" name="id" value={$deleteData.id} /> <input type="hidden" name="id" value={$deleteData.id} />
<AlertDialog.Action type="submit" variant="destructive">Delete user</AlertDialog.Action> <AlertDialog.Action type="submit" variant="destructive">Remove access</AlertDialog.Action>
</form> </form>
{/if} {/if}
</AlertDialog.Footer> </AlertDialog.Footer>
@@ -13,9 +13,9 @@
icon: MapPinIcon, icon: MapPinIcon,
items: [ items: [
{ label: 'Name', value: organization.name }, { label: 'Name', value: organization.name },
{ label: 'Workspace', value: organization.workspace }, { label: 'Address line 1', value: organization.addressLine1 },
{ label: 'Address', value: organization.addressLine1 },
{ label: 'Address line 2', value: organization.addressLine2 }, { label: 'Address line 2', value: organization.addressLine2 },
{ label: 'Address line 3', value: organization.addressLine3 },
{ label: 'City', value: organization.city }, { label: 'City', value: organization.city },
{ label: 'Region', value: organization.region }, { label: 'Region', value: organization.region },
{ label: 'Postcode', value: organization.postcode }, { label: 'Postcode', value: organization.postcode },
@@ -6,9 +6,9 @@
type Organization = { type Organization = {
id: string; id: string;
name: string; name: string;
workspace: string;
addressLine1?: string | null; addressLine1?: string | null;
addressLine2?: string | null; addressLine2?: string | null;
addressLine3?: string | null;
city?: string | null; city?: string | null;
region?: string | null; region?: string | null;
postcode?: string | null; postcode?: string | null;
@@ -21,13 +21,8 @@
bankSwift?: string | null; bankSwift?: string | null;
}; };
let { let { organization }: { organization: Organization } = $props();
organization, let open = $state(false);
open = $bindable(false)
}: {
organization: Organization;
open: boolean;
} = $props();
</script> </script>
<Dialog.Root bind:open> <Dialog.Root bind:open>
+29 -19
View File
@@ -1,6 +1,6 @@
import { redirect, error } from '@sveltejs/kit'; import { redirect, error } from '@sveltejs/kit';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { organizations } from '$lib/server/db/schema'; import { organizationMemberships, organizations } from '$lib/server/db/schema';
import { organizationCreateSchema } from '$lib/schemas/organizations.schema'; import { organizationCreateSchema } from '$lib/schemas/organizations.schema';
import { setActiveOrganization } from '$lib/server/organizations'; import { setActiveOrganization } from '$lib/server/organizations';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
@@ -24,24 +24,34 @@ export const POST: RequestHandler = async ({ locals, request }) => {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const now = new Date(); const now = new Date();
await db.insert(organizations).values({ await db.transaction(async (tx) => {
id, await tx.insert(organizations).values({
name: parsed.data.name, id,
workspace: parsed.data.workspace, name: parsed.data.name,
addressLine1: parsed.data.addressLine1 || null, addressLine1: parsed.data.addressLine1,
addressLine2: parsed.data.addressLine2 || null, addressLine2: parsed.data.addressLine2 || null,
city: parsed.data.city || null, addressLine3: parsed.data.addressLine3 || null,
region: parsed.data.region || null, city: parsed.data.city || null,
postcode: parsed.data.postcode || null, region: parsed.data.region || null,
country: parsed.data.country, postcode: parsed.data.postcode || null,
bankName: parsed.data.bankName || null, country: parsed.data.country,
bankAccountName: parsed.data.bankAccountName || null, bankName: parsed.data.bankName || null,
bankAccountNumber: parsed.data.bankAccountNumber || null, bankAccountName: parsed.data.bankAccountName || null,
bankSortCode: parsed.data.bankSortCode || null, bankAccountNumber: parsed.data.bankAccountNumber || null,
bankIban: parsed.data.bankIban || null, bankSortCode: parsed.data.bankSortCode || null,
bankSwift: parsed.data.bankSwift || null, bankIban: parsed.data.bankIban || null,
createdAt: now, bankSwift: parsed.data.bankSwift || null,
updatedAt: now createdAt: now,
updatedAt: now
});
await tx.insert(organizationMemberships).values({
organizationId: id,
userId: locals.user.id,
role: 'admin',
createdAt: now,
updatedAt: now
});
}); });
await setActiveOrganization(locals, id); await setActiveOrganization(locals, id);
@@ -2,7 +2,7 @@ import { and, eq, isNull } from 'drizzle-orm';
import { error, redirect } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { organizations } from '$lib/server/db/schema'; import { organizations } from '$lib/server/db/schema';
import { loadOrganizationContext } from '$lib/server/organizations'; import { requireActiveOrganizationAdmin } from '$lib/server/organizations';
import { organizationEditSchema } from '$lib/schemas/organizations.schema'; import { organizationEditSchema } from '$lib/schemas/organizations.schema';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
@@ -17,7 +17,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
redirect(303, '/login'); redirect(303, '/login');
} }
const { activeOrganizationId } = await loadOrganizationContext(locals); const { activeOrganizationId } = await requireActiveOrganizationAdmin(locals);
const formData = await request.formData(); const formData = await request.formData();
const parsed = organizationEditSchema.safeParse(Object.fromEntries(formData)); const parsed = organizationEditSchema.safeParse(Object.fromEntries(formData));
@@ -29,9 +29,9 @@ export const POST: RequestHandler = async ({ locals, request }) => {
.update(organizations) .update(organizations)
.set({ .set({
name: parsed.data.name, name: parsed.data.name,
workspace: parsed.data.workspace, addressLine1: parsed.data.addressLine1,
addressLine1: parsed.data.addressLine1 || null,
addressLine2: parsed.data.addressLine2 || null, addressLine2: parsed.data.addressLine2 || null,
addressLine3: parsed.data.addressLine3 || null,
city: parsed.data.city || null, city: parsed.data.city || null,
region: parsed.data.region || null, region: parsed.data.region || null,
postcode: parsed.data.postcode || null, postcode: parsed.data.postcode || null,