feat: scope organization access by memberships
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
|
||||
type Organization = {
|
||||
name: string;
|
||||
workspace: string;
|
||||
addressLine1?: string | null;
|
||||
addressLine2?: string | null;
|
||||
addressLine3?: string | null;
|
||||
city?: string | null;
|
||||
region?: string | null;
|
||||
postcode?: string | null;
|
||||
@@ -43,26 +43,17 @@
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for={`${prefix}-workspace`}>Workspace</Field.Label>
|
||||
<Input
|
||||
id={`${prefix}-workspace`}
|
||||
name="workspace"
|
||||
type="text"
|
||||
value={organization?.workspace ?? 'Blueprint Workspace'}
|
||||
required
|
||||
/>
|
||||
</Field.Field>
|
||||
</div>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label for={`${prefix}-address-line-1`}>Address</Field.Label>
|
||||
<Field.Label for={`${prefix}-address-line-1`}>Address line 1</Field.Label>
|
||||
<Input
|
||||
id={`${prefix}-address-line-1`}
|
||||
name="addressLine1"
|
||||
type="text"
|
||||
value={organization?.addressLine1 ?? ''}
|
||||
required
|
||||
/>
|
||||
</Field.Field>
|
||||
</div>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label for={`${prefix}-address-line-2`}>Address line 2</Field.Label>
|
||||
<Input
|
||||
@@ -72,6 +63,15 @@
|
||||
value={organization?.addressLine2 ?? ''}
|
||||
/>
|
||||
</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">
|
||||
<Field.Field>
|
||||
<Field.Label for={`${prefix}-city`}>City</Field.Label>
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
type Organization = {
|
||||
id: string;
|
||||
name: string;
|
||||
workspace: string;
|
||||
addressLine1?: string | null;
|
||||
membershipRole?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -48,7 +49,9 @@
|
||||
</div>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<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>
|
||||
<ChevronsUpDownIcon class="ms-auto" />
|
||||
</Sidebar.MenuButton>
|
||||
@@ -73,7 +76,7 @@
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">{organization.name}</span>
|
||||
<span class="truncate text-xs text-muted-foreground"
|
||||
>{organization.workspace}</span
|
||||
>{organization.addressLine1 || 'Address not set'}</span
|
||||
>
|
||||
</div>
|
||||
{#if organization.id === activeOrganization.id}
|
||||
@@ -116,7 +119,9 @@
|
||||
</div>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<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>
|
||||
</button>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
@@ -9,7 +9,7 @@ const roleSchema = z.enum(['admin', 'user'], 'Select a role.');
|
||||
export const createAdminSchema = z.object({
|
||||
name: z.string().trim().min(1, 'Enter a name.'),
|
||||
email: z.email('Enter a valid email address.').transform((value) => value.toLowerCase()),
|
||||
password: z.string().min(8, passwordMessage),
|
||||
password: optionalPassword,
|
||||
role: roleSchema.default('admin')
|
||||
});
|
||||
|
||||
|
||||
@@ -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 { organizations, session } from '$lib/server/db/schema';
|
||||
|
||||
const DEFAULT_ORGANIZATION_ID = 'default-organization';
|
||||
import { organizationMemberships, organizations, session } from '$lib/server/db/schema';
|
||||
|
||||
type SessionWithOrganization = {
|
||||
id: string;
|
||||
@@ -10,43 +9,46 @@ type SessionWithOrganization = {
|
||||
};
|
||||
|
||||
export async function loadOrganizationContext(locals: App.Locals) {
|
||||
if (!locals.user) {
|
||||
error(401, 'Sign in before opening the dashboard.');
|
||||
}
|
||||
|
||||
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)
|
||||
.innerJoin(
|
||||
organizationMemberships,
|
||||
and(
|
||||
eq(organizationMemberships.organizationId, organizations.id),
|
||||
eq(organizationMemberships.userId, locals.user.id),
|
||||
isNull(organizationMemberships.archivedAt)
|
||||
)
|
||||
)
|
||||
.where(isNull(organizations.archivedAt))
|
||||
.orderBy(asc(organizations.name));
|
||||
|
||||
if (rows.length === 0) {
|
||||
const now = new Date();
|
||||
|
||||
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
|
||||
});
|
||||
error(403, 'You have not been added to an organization.');
|
||||
}
|
||||
|
||||
const currentSession = locals.session as SessionWithOrganization | undefined;
|
||||
@@ -66,14 +68,45 @@ export async function loadOrganizationContext(locals: App.Locals) {
|
||||
return {
|
||||
organizations: rows,
|
||||
activeOrganization,
|
||||
activeOrganizationId: activeOrganization.id
|
||||
activeOrganizationId: activeOrganization.id,
|
||||
activeMembershipRole: activeOrganization.membershipRole
|
||||
};
|
||||
}
|
||||
|
||||
export async function setActiveOrganization(locals: App.Locals, organizationId: string) {
|
||||
if (!locals.user) return null;
|
||||
|
||||
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)
|
||||
.innerJoin(
|
||||
organizationMemberships,
|
||||
and(
|
||||
eq(organizationMemberships.organizationId, organizations.id),
|
||||
eq(organizationMemberships.userId, locals.user.id),
|
||||
isNull(organizationMemberships.archivedAt)
|
||||
)
|
||||
)
|
||||
.where(eq(organizations.id, organizationId))
|
||||
.limit(1);
|
||||
|
||||
@@ -92,3 +125,13 @@ export async function setActiveOrganization(locals: App.Locals, organizationId:
|
||||
|
||||
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;
|
||||
} = $props();
|
||||
|
||||
let editOpen = $state(false);
|
||||
|
||||
const tabs = [
|
||||
{ value: 'details', label: 'Details' },
|
||||
{ value: 'admins', label: 'Admins' }
|
||||
@@ -46,7 +44,7 @@
|
||||
Manage organization details, invoice payment information, and administrators.
|
||||
</p>
|
||||
</div>
|
||||
<OrganizationEditDialog organization={data.activeOrganization} bind:open={editOpen} />
|
||||
<OrganizationEditDialog organization={data.activeOrganization} />
|
||||
</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 { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { auth } from '$lib/server/auth';
|
||||
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 type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const { activeOrganizationId } = await requireActiveOrganizationAdmin(locals);
|
||||
const users = await db
|
||||
.select({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
role: organizationMemberships.role,
|
||||
banned: user.banned,
|
||||
emailVerified: user.emailVerified,
|
||||
createdAt: user.createdAt,
|
||||
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));
|
||||
|
||||
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 = {
|
||||
create: async (event) => {
|
||||
const { activeOrganizationId } = await requireActiveOrganizationAdmin(event.locals);
|
||||
const form = await superValidate(event, zod4(createAdminSchema), { id: 'create-admin' });
|
||||
|
||||
if (!form.valid) {
|
||||
@@ -46,8 +71,20 @@ export const actions: Actions = {
|
||||
}
|
||||
|
||||
try {
|
||||
const [existingUser] = await db
|
||||
.select({ id: user.id })
|
||||
.from(user)
|
||||
.where(eq(user.email, form.data.email))
|
||||
.limit(1);
|
||||
|
||||
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({
|
||||
headers: event.request.headers,
|
||||
body: {
|
||||
name: form.data.name,
|
||||
email: form.data.email,
|
||||
@@ -55,25 +92,87 @@ export const actions: Actions = {
|
||||
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 {
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
return message(form, 'Admin created.');
|
||||
return message(form, 'User added to this organization.');
|
||||
},
|
||||
|
||||
edit: async (event) => {
|
||||
const { activeOrganizationId } = await requireActiveOrganizationAdmin(event.locals);
|
||||
const form = await superValidate(event, zod4(editAdminSchema), { id: 'edit-admin' });
|
||||
|
||||
if (!form.valid) {
|
||||
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 {
|
||||
await auth.api.adminUpdateUser({
|
||||
headers: event.request.headers,
|
||||
body: {
|
||||
userId: form.data.id,
|
||||
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) {
|
||||
await auth.api.setUserPassword({
|
||||
headers: event.request.headers,
|
||||
body: {
|
||||
userId: form.data.id,
|
||||
newPassword: form.data.password
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.update(organizationMemberships)
|
||||
.set({ role: form.data.role, updatedAt: new Date() })
|
||||
.where(eq(organizationMemberships.id, membership.id));
|
||||
} 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
|
||||
});
|
||||
}
|
||||
|
||||
return message(form, 'Admin updated.');
|
||||
return message(form, 'User updated.');
|
||||
},
|
||||
|
||||
delete: async (event) => {
|
||||
const { activeOrganizationId } = await requireActiveOrganizationAdmin(event.locals);
|
||||
const form = await superValidate(event, zod4(deleteAdminSchema), { id: 'delete-admin' });
|
||||
|
||||
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>
|
||||
<h2 class="text-xl font-semibold tracking-tight">Administrators</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<DropdownMenu.Content align="end" class="w-36">
|
||||
<DropdownMenu.Item onclick={() => openEdit(user)}>Edit</DropdownMenu.Item>
|
||||
<DropdownMenu.Item variant="destructive" onclick={() => openDelete(user)}>
|
||||
Delete
|
||||
Remove access
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
<Dialog.Content class="sm:max-w-md">
|
||||
<Dialog.Header>
|
||||
<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>
|
||||
<form method="POST" action="?/create" class="grid gap-2" use:enhanceCreate>
|
||||
<FormField form={createForm} name="name">
|
||||
@@ -66,6 +68,7 @@
|
||||
{...props}
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
placeholder="Required for new users"
|
||||
bind:value={$createData.password}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<AlertDialog.Root open={!!deletingUser} onOpenChange={(open) => !open && onClose()}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Delete user?</AlertDialog.Title>
|
||||
<AlertDialog.Title>Remove access?</AlertDialog.Title>
|
||||
<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.Header>
|
||||
<AlertDialog.Footer>
|
||||
@@ -27,7 +27,7 @@
|
||||
{#if deletingUser}
|
||||
<form method="POST" action="?/delete" use:enhanceDelete>
|
||||
<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>
|
||||
{/if}
|
||||
</AlertDialog.Footer>
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
icon: MapPinIcon,
|
||||
items: [
|
||||
{ label: 'Name', value: organization.name },
|
||||
{ label: 'Workspace', value: organization.workspace },
|
||||
{ label: 'Address', value: organization.addressLine1 },
|
||||
{ label: 'Address line 1', value: organization.addressLine1 },
|
||||
{ label: 'Address line 2', value: organization.addressLine2 },
|
||||
{ label: 'Address line 3', value: organization.addressLine3 },
|
||||
{ label: 'City', value: organization.city },
|
||||
{ label: 'Region', value: organization.region },
|
||||
{ label: 'Postcode', value: organization.postcode },
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
type Organization = {
|
||||
id: string;
|
||||
name: string;
|
||||
workspace: string;
|
||||
addressLine1?: string | null;
|
||||
addressLine2?: string | null;
|
||||
addressLine3?: string | null;
|
||||
city?: string | null;
|
||||
region?: string | null;
|
||||
postcode?: string | null;
|
||||
@@ -21,13 +21,8 @@
|
||||
bankSwift?: string | null;
|
||||
};
|
||||
|
||||
let {
|
||||
organization,
|
||||
open = $bindable(false)
|
||||
}: {
|
||||
organization: Organization;
|
||||
open: boolean;
|
||||
} = $props();
|
||||
let { organization }: { organization: Organization } = $props();
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
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 { setActiveOrganization } from '$lib/server/organizations';
|
||||
import type { RequestHandler } from './$types';
|
||||
@@ -24,12 +24,13 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date();
|
||||
|
||||
await db.insert(organizations).values({
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(organizations).values({
|
||||
id,
|
||||
name: parsed.data.name,
|
||||
workspace: parsed.data.workspace,
|
||||
addressLine1: parsed.data.addressLine1 || null,
|
||||
addressLine1: parsed.data.addressLine1,
|
||||
addressLine2: parsed.data.addressLine2 || null,
|
||||
addressLine3: parsed.data.addressLine3 || null,
|
||||
city: parsed.data.city || null,
|
||||
region: parsed.data.region || null,
|
||||
postcode: parsed.data.postcode || null,
|
||||
@@ -44,6 +45,15 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
updatedAt: now
|
||||
});
|
||||
|
||||
await tx.insert(organizationMemberships).values({
|
||||
organizationId: id,
|
||||
userId: locals.user.id,
|
||||
role: 'admin',
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
});
|
||||
});
|
||||
|
||||
await setActiveOrganization(locals, id);
|
||||
|
||||
redirect(303, safeRedirect(parsed.data.redirectTo));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
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 type { RequestHandler } from './$types';
|
||||
|
||||
@@ -17,7 +17,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
redirect(303, '/login');
|
||||
}
|
||||
|
||||
const { activeOrganizationId } = await loadOrganizationContext(locals);
|
||||
const { activeOrganizationId } = await requireActiveOrganizationAdmin(locals);
|
||||
const formData = await request.formData();
|
||||
const parsed = organizationEditSchema.safeParse(Object.fromEntries(formData));
|
||||
|
||||
@@ -29,9 +29,9 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
||||
.update(organizations)
|
||||
.set({
|
||||
name: parsed.data.name,
|
||||
workspace: parsed.data.workspace,
|
||||
addressLine1: parsed.data.addressLine1 || null,
|
||||
addressLine1: parsed.data.addressLine1,
|
||||
addressLine2: parsed.data.addressLine2 || null,
|
||||
addressLine3: parsed.data.addressLine3 || null,
|
||||
city: parsed.data.city || null,
|
||||
region: parsed.data.region || null,
|
||||
postcode: parsed.data.postcode || null,
|
||||
|
||||
Reference in New Issue
Block a user