feat: add organization context model
Introduce organizations as the top-level workspace model with address and bank-detail fields. Store the active organization on Better Auth sessions and load organization context for dashboard routes. Add the migration that creates organizations and backfills existing records to the default organization.
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
CREATE TABLE `organizations` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`workspace` text DEFAULT 'Default Workspace' NOT NULL,
|
||||||
|
`address_line_1` text,
|
||||||
|
`address_line_2` text,
|
||||||
|
`city` text,
|
||||||
|
`region` text,
|
||||||
|
`postcode` text,
|
||||||
|
`country` text DEFAULT 'United Kingdom' NOT NULL,
|
||||||
|
`bank_name` text,
|
||||||
|
`bank_account_name` text,
|
||||||
|
`bank_account_number` text,
|
||||||
|
`bank_sort_code` text,
|
||||||
|
`bank_iban` text,
|
||||||
|
`bank_swift` text,
|
||||||
|
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||||
|
`archived_at` integer
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `organizations` (`id`, `name`, `workspace`) VALUES ('default-organization', 'Steel City Stadium', 'Blueprint Workspace');--> statement-breakpoint
|
||||||
|
ALTER TABLE `clients` ADD `organization_id` text NOT NULL DEFAULT 'default-organization' REFERENCES organizations(id);--> statement-breakpoint
|
||||||
|
ALTER TABLE `addresses` ADD `organization_id` text NOT NULL DEFAULT 'default-organization' REFERENCES organizations(id);--> statement-breakpoint
|
||||||
|
CREATE INDEX `addresses_organization_id_idx` ON `addresses` (`organization_id`);--> statement-breakpoint
|
||||||
|
ALTER TABLE `contacts` ADD `organization_id` text NOT NULL DEFAULT 'default-organization' REFERENCES organizations(id);--> statement-breakpoint
|
||||||
|
CREATE INDEX `contacts_organization_id_idx` ON `contacts` (`organization_id`);--> statement-breakpoint
|
||||||
|
ALTER TABLE `rooms` ADD `organization_id` text NOT NULL DEFAULT 'default-organization' REFERENCES organizations(id);--> statement-breakpoint
|
||||||
|
ALTER TABLE `services` ADD `organization_id` text NOT NULL DEFAULT 'default-organization' REFERENCES organizations(id);--> statement-breakpoint
|
||||||
|
ALTER TABLE `bookings` ADD `organization_id` text NOT NULL DEFAULT 'default-organization' REFERENCES organizations(id);--> statement-breakpoint
|
||||||
|
CREATE INDEX `bookings_organization_id_idx` ON `bookings` (`organization_id`);--> statement-breakpoint
|
||||||
|
ALTER TABLE `invoices` ADD `organization_id` text NOT NULL DEFAULT 'default-organization' REFERENCES organizations(id);--> statement-breakpoint
|
||||||
|
CREATE INDEX `invoices_organization_id_idx` ON `invoices` (`organization_id`);--> statement-breakpoint
|
||||||
|
ALTER TABLE `contracts` ADD `organization_id` text NOT NULL DEFAULT 'default-organization' REFERENCES organizations(id);--> statement-breakpoint
|
||||||
|
CREATE INDEX `contracts_organization_id_idx` ON `contracts` (`organization_id`);--> statement-breakpoint
|
||||||
|
ALTER TABLE `session` ADD `active_organization_id` text;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,13 @@
|
|||||||
"when": 1780699922786,
|
"when": 1780699922786,
|
||||||
"tag": "0005_bright_wiccan",
|
"tag": "0005_bright_wiccan",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1780750080744,
|
||||||
|
"tag": "0006_cloudy_catseye",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Vendored
+1
-1
@@ -13,7 +13,7 @@ declare global {
|
|||||||
|
|
||||||
interface Locals {
|
interface Locals {
|
||||||
user?: User;
|
user?: User;
|
||||||
session?: Session;
|
session?: Session & { activeOrganizationId?: string | null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ export const auth = betterAuth({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
disableSignUp: true
|
disableSignUp: true
|
||||||
},
|
},
|
||||||
|
session: {
|
||||||
|
additionalFields: {
|
||||||
|
activeOrganizationId: {
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
input: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
admin(),
|
admin(),
|
||||||
sveltekitCookies(getRequestEvent) // make sure this is the last plugin in the array
|
sveltekitCookies(getRequestEvent) // make sure this is the last plugin in the array
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export const session = sqliteTable(
|
|||||||
userId: text('user_id')
|
userId: text('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id, { onDelete: 'cascade' }),
|
.references(() => user.id, { onDelete: 'cascade' }),
|
||||||
impersonatedBy: text('impersonated_by')
|
impersonatedBy: text('impersonated_by'),
|
||||||
|
activeOrganizationId: text('active_organization_id')
|
||||||
},
|
},
|
||||||
(table) => [index('session_userId_idx').on(table.userId)]
|
(table) => [index('session_userId_idx').on(table.userId)]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||||
|
import { timestamps } from './shared.schema';
|
||||||
|
|
||||||
|
export const organizations = sqliteTable('organizations', {
|
||||||
|
id: text('id')
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => crypto.randomUUID()),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
workspace: text('workspace').notNull().default('Default Workspace'),
|
||||||
|
addressLine1: text('address_line_1'),
|
||||||
|
addressLine2: text('address_line_2'),
|
||||||
|
city: text('city'),
|
||||||
|
region: text('region'),
|
||||||
|
postcode: text('postcode'),
|
||||||
|
country: text('country').notNull().default('United Kingdom'),
|
||||||
|
bankName: text('bank_name'),
|
||||||
|
bankAccountName: text('bank_account_name'),
|
||||||
|
bankAccountNumber: text('bank_account_number'),
|
||||||
|
bankSortCode: text('bank_sort_code'),
|
||||||
|
bankIban: text('bank_iban'),
|
||||||
|
bankSwift: text('bank_swift'),
|
||||||
|
...timestamps
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ export const task = sqliteTable('task', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export * from './clients.schema';
|
export * from './clients.schema';
|
||||||
|
export * from './organizations.schema';
|
||||||
export * from './addresses.schema';
|
export * from './addresses.schema';
|
||||||
export * from './contacts.schema';
|
export * from './contacts.schema';
|
||||||
export * from './rooms.schema';
|
export * from './rooms.schema';
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { 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';
|
||||||
|
|
||||||
|
type SessionWithOrganization = {
|
||||||
|
id: string;
|
||||||
|
activeOrganizationId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadOrganizationContext(locals: App.Locals) {
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(organizations)
|
||||||
|
.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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSession = locals.session as SessionWithOrganization | undefined;
|
||||||
|
const activeOrganization =
|
||||||
|
rows.find((organization) => organization.id === currentSession?.activeOrganizationId) ??
|
||||||
|
rows[0];
|
||||||
|
|
||||||
|
if (currentSession && currentSession.activeOrganizationId !== activeOrganization.id) {
|
||||||
|
await db
|
||||||
|
.update(session)
|
||||||
|
.set({ activeOrganizationId: activeOrganization.id, updatedAt: new Date() })
|
||||||
|
.where(eq(session.id, currentSession.id));
|
||||||
|
|
||||||
|
currentSession.activeOrganizationId = activeOrganization.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizations: rows,
|
||||||
|
activeOrganization,
|
||||||
|
activeOrganizationId: activeOrganization.id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setActiveOrganization(locals: App.Locals, organizationId: string) {
|
||||||
|
const [organization] = await db
|
||||||
|
.select()
|
||||||
|
.from(organizations)
|
||||||
|
.where(eq(organizations.id, organizationId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const currentSession = locals.session as SessionWithOrganization | undefined;
|
||||||
|
|
||||||
|
if (!organization || organization.archivedAt || !currentSession) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(session)
|
||||||
|
.set({ activeOrganizationId: organization.id, updatedAt: new Date() })
|
||||||
|
.where(eq(session.id, currentSession.id));
|
||||||
|
|
||||||
|
currentSession.activeOrganizationId = organization.id;
|
||||||
|
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { loadOrganizationContext } from '$lib/server/organizations';
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
|
if (!locals.user) {
|
||||||
|
redirect(303, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadOrganizationContext(locals);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user