From b4ded88ba067ba9c47e5a1a6a4dcc64da4e2a1cd Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Fri, 21 Aug 2026 13:02:54 +0200 Subject: [PATCH 01/19] base organization module --- src/hooks.server.ts | 3 + src/lib/database/schemas.ts | 13 ++ .../application/organization.service.test.ts | 197 ++++++++++++++++++ .../application/organization.service.ts | 146 ++++++++++++- .../domain/organization.domain.ts | 25 +++ src/modules/organization/index.ts | 12 +- .../repositories/organization.repository.ts | 52 +++++ 7 files changed, 443 insertions(+), 5 deletions(-) create mode 100644 src/modules/organization/application/organization.service.test.ts create mode 100644 src/modules/organization/domain/organization.domain.ts create mode 100644 src/modules/organization/infrastructure/repositories/organization.repository.ts diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 02dcc9c..b725525 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,5 +1,6 @@ import type { Handle } from '@sveltejs/kit'; import { authService, canAccessAdminArea, ensureAuthReady } from './modules/auth'; +import { ensureOrganizationReady } from './modules/organization'; import { getGitDb } from '$lib/server/gitdb'; getGitDb(); @@ -10,6 +11,8 @@ export const handle: Handle = async ({ event, resolve }) => { } await ensureAuthReady(); + await ensureOrganizationReady(); + const sessionCookie = event.cookies.get('pos_session'); const currentUser = await authService.resolveAuthenticatedUser(sessionCookie); diff --git a/src/lib/database/schemas.ts b/src/lib/database/schemas.ts index dd5c620..8ed437a 100644 --- a/src/lib/database/schemas.ts +++ b/src/lib/database/schemas.ts @@ -100,3 +100,16 @@ relations.for(ProjectEntity, ({ many }) => ({ relations.for(ProjectRoleEntity, ({ one }) => ({ project: one(ProjectEntity, { fields: ['projectId'], references: ['id'] }), })); + +export const OrganizationEntity = entity('organizations', { + id: uuid().primaryKey(), + slug: text().notNull(), + name: text().notNull(), + description: text(), + createdAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), + updatedAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), +}); diff --git a/src/modules/organization/application/organization.service.test.ts b/src/modules/organization/application/organization.service.test.ts new file mode 100644 index 0000000..ab7e0b7 --- /dev/null +++ b/src/modules/organization/application/organization.service.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it, beforeEach } from 'vitest'; +import { OrganizationService } from './organization.service'; +import { OrganizationDomain } from '../domain/organization.domain'; + +class FakeOrganizationRepository { + rows: OrganizationDomain[] = []; + + async findAll() { + return [...this.rows]; + } + + async findById(id: string) { + return this.rows.find((o) => o.id === id) ?? null; + } + + async findBySlug(slug: string) { + return this.rows.find((o) => o.slug === slug) ?? null; + } + + async create(input: { id: string; slug: string; name: string; description?: string }) { + this.rows.push( + new OrganizationDomain({ + id: input.id, + slug: input.slug, + name: input.name, + description: input.description, + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }), + ); + } + + async update(id: string, changes: { name?: string; slug?: string; description?: string }) { + const organization = this.rows.find((o) => o.id === id); + if (!organization) return; + if (changes.name !== undefined) organization.name = changes.name; + if (changes.slug !== undefined) organization.slug = changes.slug; + if (changes.description !== undefined) organization.description = changes.description; + } + + async deleteById(id: string) { + this.rows = this.rows.filter((o) => o.id !== id); + } +} + +describe('OrganizationService', () => { + let repository: FakeOrganizationRepository; + let service: OrganizationService; + + beforeEach(() => { + repository = new FakeOrganizationRepository(); + service = new OrganizationService(repository as any); + }); + + describe('listOrganizations', () => { + it('returns an empty list when there are no organizations', async () => { + expect(await service.listOrganizations()).toEqual([]); + }); + + it('lists created organizations', async () => { + await service.createOrganization({ name: 'GitOps' }); + const organizations = await service.listOrganizations(); + expect(organizations).toHaveLength(1); + expect(organizations[0].slug).toBe('gitops'); + }); + }); + + describe('getOrganization / findBySlug', () => { + it('throws when the organization does not exist', async () => { + await expect(service.getOrganization('missing-id')).rejects.toThrow(/not found/); + await expect(service.findBySlug('missing-slug')).rejects.toThrow(/not found/); + }); + + it('returns the organization by id and by slug', async () => { + const created = await service.createOrganization({ name: 'Kettu' }); + + const byId = await service.getOrganization(created.id); + expect(byId.slug).toBe('kettu'); + + const bySlug = await service.findBySlug('kettu'); + expect(bySlug.id).toBe(created.id); + }); + }); + + describe('createOrganization', () => { + it('requires a non-empty name', async () => { + await expect(service.createOrganization({ name: ' ' })).rejects.toThrow( + /name is required/, + ); + }); + + it('auto-generates a normalized slug from the name when none is provided', async () => { + const organization = await service.createOrganization({ name: 'Kettu Studio!!' }); + expect(organization.slug).toBe('kettu-studio'); + }); + + it('uses the provided slug, normalized', async () => { + const organization = await service.createOrganization({ + name: 'Kettu Studio', + slug: 'Custom Slug', + }); + expect(organization.slug).toBe('custom-slug'); + }); + + it('rejects creating an organization with a duplicate slug', async () => { + await service.createOrganization({ name: 'Kettu Studio' }); + await expect(service.createOrganization({ name: 'Kettu Studio' })).rejects.toThrow( + /already exists/, + ); + }); + + it('trims the optional description', async () => { + const organization = await service.createOrganization({ + name: 'Kettu Studio', + description: ' A studio ', + }); + expect(organization.description).toBe('A studio'); + }); + }); + + describe('updateOrganization', () => { + it('throws when the organization does not exist', async () => { + await expect(service.updateOrganization('missing-id', { name: 'X' })).rejects.toThrow( + /not found/, + ); + }); + + it('updates only the fields provided', async () => { + const created = await service.createOrganization({ name: 'Original Name' }); + + const updated = await service.updateOrganization(created.id, { + description: 'New description', + }); + expect(updated.name).toBe('Original Name'); + expect(updated.description).toBe('New description'); + }); + + it('rejects clearing the name', async () => { + const created = await service.createOrganization({ name: 'Original Name' }); + await expect(service.updateOrganization(created.id, { name: ' ' })).rejects.toThrow( + /name is required/, + ); + }); + + it('normalizes the slug when updating it', async () => { + const created = await service.createOrganization({ name: 'Original Name' }); + const updated = await service.updateOrganization(created.id, { slug: 'New Slug!!' }); + expect(updated.slug).toBe('new-slug'); + }); + + it('rejects updating to a slug already used by another organization', async () => { + await service.createOrganization({ name: 'Organization A', slug: 'taken' }); + const created = await service.createOrganization({ name: 'Organization B' }); + + await expect(service.updateOrganization(created.id, { slug: 'taken' })).rejects.toThrow( + /already exists/, + ); + }); + + it('allows keeping the same slug on the same organization', async () => { + const created = await service.createOrganization({ + name: 'Organization A', + slug: 'same-slug', + }); + const updated = await service.updateOrganization(created.id, { slug: 'same-slug' }); + expect(updated.slug).toBe('same-slug'); + }); + }); + + describe('deleteOrganization', () => { + it('throws when the organization does not exist', async () => { + await expect(service.deleteOrganization('missing-id')).rejects.toThrow(/not found/); + }); + + it('removes the organization', async () => { + const created = await service.createOrganization({ name: 'Organization A' }); + await service.deleteOrganization(created.id); + await expect(service.getOrganization(created.id)).rejects.toThrow(/not found/); + }); + }); + + describe('bootstrapDefaults', () => { + it('seeds the default gitops organization when none exists', async () => { + await service.bootstrapDefaults(); + const organizations = await service.listOrganizations(); + expect(organizations).toHaveLength(1); + expect(organizations[0].slug).toBe('gitops'); + }); + + it('does not create a duplicate when the default organization already exists', async () => { + await service.bootstrapDefaults(); + await service.bootstrapDefaults(); + const organizations = await service.listOrganizations(); + expect(organizations).toHaveLength(1); + }); + }); +}); diff --git a/src/modules/organization/application/organization.service.ts b/src/modules/organization/application/organization.service.ts index 71d885b..6c5bd30 100644 --- a/src/modules/organization/application/organization.service.ts +++ b/src/modules/organization/application/organization.service.ts @@ -1,8 +1,146 @@ -export type Organization = { id: string; name: string; slug: string }; +import crypto from 'crypto'; +import { OrganizationRepository } from '../infrastructure/repositories/organization.repository'; + +export type Organization = { + id: string; + name: string; + slug: string; + description?: string | null; + createdAt: Date; + updatedAt: Date; +}; + +const DEFAULT_ORGANIZATION = { name: 'GitOps', slug: 'gitops' }; export class OrganizationService { - // stub: no persistence yet, always resolves to the single default organization - async findBySlug(_slug: string): Promise { - return { id: 'gitops', name: 'GitOps', slug: 'gitops' }; + constructor(private readonly repository: OrganizationRepository) {} + + async listOrganizations() { + const organizations = await this.repository.findAll(); + return organizations.map((organization) => organization.toJson()); + } + + async getOrganization(id: string) { + const organization = await this.repository.findById(id); + if (!organization) { + throw new Error('Organization not found'); + } + return organization.toJson(); + } + + async findBySlug(slug: string) { + const organization = await this.repository.findBySlug(slug); + if (!organization) { + throw new Error('Organization not found'); + } + return organization.toJson(); + } + + async createOrganization(input: { name: string; slug?: string; description?: string }) { + const name = input.name.trim(); + if (!name) { + throw new Error('Organization name is required'); + } + + const slug = this.normalizeSlug(input.slug || name); + if (!slug) { + throw new Error('Organization slug is required'); + } + + const existing = await this.repository.findBySlug(slug); + if (existing) { + throw new Error('An organization with this slug already exists'); + } + + await this.repository.create({ + id: crypto.randomUUID(), + slug, + name, + description: input.description?.trim() || undefined, + }); + + const created = await this.repository.findBySlug(slug); + if (!created) { + throw new Error('Failed to create organization'); + } + + return created.toJson(); + } + + async updateOrganization( + id: string, + changes: { name?: string; slug?: string; description?: string }, + ) { + const organization = await this.repository.findById(id); + if (!organization) { + throw new Error('Organization not found'); + } + + const patch: { name?: string; slug?: string; description?: string } = {}; + + if (changes.name !== undefined) { + const name = changes.name.trim(); + if (!name) { + throw new Error('Organization name is required'); + } + patch.name = name; + } + + if (changes.slug !== undefined) { + const slug = this.normalizeSlug(changes.slug); + if (!slug) { + throw new Error('Organization slug is required'); + } + + const existing = await this.repository.findBySlug(slug); + if (existing && existing.id !== id) { + throw new Error('An organization with this slug already exists'); + } + patch.slug = slug; + } + + if (changes.description !== undefined) { + patch.description = changes.description.trim(); + } + + await this.repository.update(id, patch); + + const updated = await this.repository.findById(id); + if (!updated) { + throw new Error('Failed to update organization'); + } + + return updated.toJson(); + } + + async deleteOrganization(id: string) { + const organization = await this.repository.findById(id); + if (!organization) { + throw new Error('Organization not found'); + } + + await this.repository.deleteById(id); + } + + // seeds the single default org so existing routes keep resolving 'gitops' out of the box + async bootstrapDefaults(): Promise { + const existing = await this.repository.findBySlug(DEFAULT_ORGANIZATION.slug); + if (existing) { + return; + } + + await this.repository.create({ + id: crypto.randomUUID(), + slug: DEFAULT_ORGANIZATION.slug, + name: DEFAULT_ORGANIZATION.name, + }); + } + + private normalizeSlug(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); } } diff --git a/src/modules/organization/domain/organization.domain.ts b/src/modules/organization/domain/organization.domain.ts new file mode 100644 index 0000000..5daf65a --- /dev/null +++ b/src/modules/organization/domain/organization.domain.ts @@ -0,0 +1,25 @@ +import { Domain } from '$lib/server/domain/domain'; + +export class OrganizationDomain extends Domain { + public name: string = ''; + public slug: string = ''; + public description?: string | null = null; + + constructor(data: any) { + super(data); + this.name = data.name; + this.slug = data.slug; + this.description = data.description; + } + + toJson() { + return { + id: this.id, + name: this.name, + slug: this.slug, + description: this.description, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + }; + } +} diff --git a/src/modules/organization/index.ts b/src/modules/organization/index.ts index 635db93..e76e3c5 100644 --- a/src/modules/organization/index.ts +++ b/src/modules/organization/index.ts @@ -1,4 +1,14 @@ import { OrganizationService } from './application/organization.service'; +import { OrganizationRepository } from './infrastructure/repositories/organization.repository'; -export const organizationService = new OrganizationService(); +const organizationRepository = new OrganizationRepository(); + +export const organizationService = new OrganizationService(organizationRepository); export type { Organization } from './application/organization.service'; + +// Bootstrap the default org once at startup. +const organizationBootstrap = organizationService.bootstrapDefaults(); + +export async function ensureOrganizationReady(): Promise { + await organizationBootstrap; +} diff --git a/src/modules/organization/infrastructure/repositories/organization.repository.ts b/src/modules/organization/infrastructure/repositories/organization.repository.ts new file mode 100644 index 0000000..8193bc6 --- /dev/null +++ b/src/modules/organization/infrastructure/repositories/organization.repository.ts @@ -0,0 +1,52 @@ +import { Repository } from '$lib/server/infra/repository'; +import { OrganizationDomain } from '../../domain/organization.domain'; +import { OrganizationEntity } from '$lib/database/schemas'; + +export class OrganizationRepository extends Repository { + async findAll(): Promise { + const result = await this.db.select().from(OrganizationEntity).orderBy('createdAt', 'asc'); + return result.rows.map((row: any) => new OrganizationDomain(row)); + } + + async findById(id: string): Promise { + const result = await this.db.select().from(OrganizationEntity).where({ id }).limit(1); + const row = result.rows[0]; + return row ? new OrganizationDomain(row) : null; + } + + async findBySlug(slug: string): Promise { + const result = await this.db.select().from(OrganizationEntity).where({ slug }).limit(1); + const row = result.rows[0]; + return row ? new OrganizationDomain(row) : null; + } + + async create(input: { + id: string; + slug: string; + name: string; + description?: string; + }): Promise { + await this.db.insert(OrganizationEntity).values({ + id: input.id, + slug: input.slug, + name: input.name, + description: input.description, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + } + + async update( + id: string, + changes: { name?: string; slug?: string; description?: string }, + ): Promise { + await this.db + .update(OrganizationEntity) + .set({ ...changes, updatedAt: new Date().toISOString() }) + .where({ id }); + } + + async deleteById(id: string): Promise { + await this.db.delete(OrganizationEntity).where({ id }); + } +} From bc2df0a2d921c8185eea9dac2f970a1db32442a3 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Fri, 21 Aug 2026 13:11:16 +0200 Subject: [PATCH 02/19] cluster settings --- src/hooks.server.ts | 7 +- src/lib/components/AppSidebar.svelte | 6 + src/routes/api/organizations/+server.ts | 42 ++ src/routes/api/organizations/[id]/+server.ts | 56 +++ src/routes/cluster-settings/+layout.svelte | 14 + src/routes/cluster-settings/+page.server.ts | 5 + src/routes/cluster-settings/orgs/+page.svelte | 410 ++++++++++++++++++ .../orgs/[org]/+page.server.ts | 11 + .../cluster-settings/orgs/[org]/+page.svelte | 272 ++++++++++++ 9 files changed, 822 insertions(+), 1 deletion(-) create mode 100644 src/routes/api/organizations/+server.ts create mode 100644 src/routes/api/organizations/[id]/+server.ts create mode 100644 src/routes/cluster-settings/+layout.svelte create mode 100644 src/routes/cluster-settings/+page.server.ts create mode 100644 src/routes/cluster-settings/orgs/+page.svelte create mode 100644 src/routes/cluster-settings/orgs/[org]/+page.server.ts create mode 100644 src/routes/cluster-settings/orgs/[org]/+page.svelte diff --git a/src/hooks.server.ts b/src/hooks.server.ts index b725525..e2d048a 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -25,7 +25,12 @@ export const handle: Handle = async ({ event, resolve }) => { return new Response(null, { status: 302, headers: { location: '/login' } }); } - if (event.url.pathname.startsWith('/settings') || event.url.pathname.startsWith('/api/system')) { + if ( + event.url.pathname.startsWith('/settings') || + event.url.pathname.startsWith('/cluster-settings') || + event.url.pathname.startsWith('/api/system') || + event.url.pathname.startsWith('/api/organizations') + ) { if (!canAccessAdminArea(currentUser)) { return new Response(null, { status: 302, headers: { location: '/' } }); } diff --git a/src/lib/components/AppSidebar.svelte b/src/lib/components/AppSidebar.svelte index fe63604..8beba3b 100644 --- a/src/lib/components/AppSidebar.svelte +++ b/src/lib/components/AppSidebar.svelte @@ -3,6 +3,7 @@ import { page } from '$app/stores'; import { BarChart3, + Building2, ChevronDown, ChevronRight, Database, @@ -157,6 +158,11 @@ { label: 'Server Access Keys', href: '/settings/server-access-keys', icon: KeyRound }, ], }, + { + name: 'Cluster Settings', + icon: Building2, + items: [{ label: 'Organizations', href: '/cluster-settings/orgs', icon: Building2 }], + }, ], }, ] satisfies NavCategory[]; diff --git a/src/routes/api/organizations/+server.ts b/src/routes/api/organizations/+server.ts new file mode 100644 index 0000000..5cbdc34 --- /dev/null +++ b/src/routes/api/organizations/+server.ts @@ -0,0 +1,42 @@ +import { json } from '@sveltejs/kit'; +import { organizationService } from '../../../modules/organization'; +import { isAdmin } from '../../../modules/auth'; + +export async function GET({ locals }) { + if (!isAdmin(locals.user)) { + return json({ error: 'Forbidden' }, { status: 403 }); + } + + try { + const organizations = await organizationService.listOrganizations(); + return json({ organizations }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return json({ error: message }, { status: 500 }); + } +} + +export async function POST({ request, locals }) { + if (!isAdmin(locals.user)) { + return json({ error: 'Forbidden' }, { status: 403 }); + } + + try { + const data = (await request.json()) as { + name?: string; + slug?: string; + description?: string; + }; + + const organization = await organizationService.createOrganization({ + name: String(data.name || ''), + slug: data.slug ? String(data.slug) : undefined, + description: data.description ? String(data.description) : undefined, + }); + + return json({ success: true, organization }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return json({ error: message }, { status: 400 }); + } +} diff --git a/src/routes/api/organizations/[id]/+server.ts b/src/routes/api/organizations/[id]/+server.ts new file mode 100644 index 0000000..ded31a4 --- /dev/null +++ b/src/routes/api/organizations/[id]/+server.ts @@ -0,0 +1,56 @@ +import { json } from '@sveltejs/kit'; +import { organizationService } from '../../../../modules/organization'; +import { isAdmin } from '../../../../modules/auth'; + +export async function GET({ params, locals }) { + if (!isAdmin(locals.user)) { + return json({ error: 'Forbidden' }, { status: 403 }); + } + + try { + const organization = await organizationService.getOrganization(params.id); + return json({ organization }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return json({ error: message }, { status: 404 }); + } +} + +export async function PATCH({ request, params, locals }) { + if (!isAdmin(locals.user)) { + return json({ error: 'Forbidden' }, { status: 403 }); + } + + try { + const data = (await request.json()) as { + name?: string; + slug?: string; + description?: string; + }; + + const organization = await organizationService.updateOrganization(params.id, { + name: data.name, + slug: data.slug, + description: data.description, + }); + + return json({ success: true, organization }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return json({ error: message }, { status: 400 }); + } +} + +export async function DELETE({ params, locals }) { + if (!isAdmin(locals.user)) { + return json({ error: 'Forbidden' }, { status: 403 }); + } + + try { + await organizationService.deleteOrganization(params.id); + return json({ success: true }); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error'; + return json({ error: message }, { status: 400 }); + } +} diff --git a/src/routes/cluster-settings/+layout.svelte b/src/routes/cluster-settings/+layout.svelte new file mode 100644 index 0000000..0221b9a --- /dev/null +++ b/src/routes/cluster-settings/+layout.svelte @@ -0,0 +1,14 @@ + + Cluster Settings - GitVault Suite + + +
+
+
+

Cluster Settings

+
+
+ +
+
+
diff --git a/src/routes/cluster-settings/+page.server.ts b/src/routes/cluster-settings/+page.server.ts new file mode 100644 index 0000000..6802e6c --- /dev/null +++ b/src/routes/cluster-settings/+page.server.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export function load() { + throw redirect(302, '/cluster-settings/orgs'); +} diff --git a/src/routes/cluster-settings/orgs/+page.svelte b/src/routes/cluster-settings/orgs/+page.svelte new file mode 100644 index 0000000..a481aac --- /dev/null +++ b/src/routes/cluster-settings/orgs/+page.svelte @@ -0,0 +1,410 @@ + + + + Organizations - Cluster Settings + + +
+
+
+

Organizations

+

Create, view and manage cluster organizations.

+
+ + +
+ +
+
+ + +
+
+ + {#if error} +
+ {error} +
+ {/if} + + {#if success} +
+ {success} +
+ {/if} + + {#if loading} +
+ Loading organizations... +
+ {:else if filteredOrganizations.length === 0} +
+ {organizations.length === 0 + ? 'No organizations found.' + : 'No organizations match your search.'} +
+ {:else} +
+
+ + + + + + + + + + + {#each filteredOrganizations as organization (organization.id)} + + + + + + + {/each} + +
NameSlugCreatedActions
+
+ +
+

{organization.name}

+ {#if organization.description} +

{organization.description}

+ {/if} +
+
+
{organization.slug}{formatDate(organization.createdAt)} +
+ + + View + + +
+
+
+
+ {/if} +
+ +{#if createModalOpen} + +
+ +
+{/if} + +{#if deleteModalOrganization} + +
+ +
+{/if} diff --git a/src/routes/cluster-settings/orgs/[org]/+page.server.ts b/src/routes/cluster-settings/orgs/[org]/+page.server.ts new file mode 100644 index 0000000..a3fdccc --- /dev/null +++ b/src/routes/cluster-settings/orgs/[org]/+page.server.ts @@ -0,0 +1,11 @@ +import { error } from '@sveltejs/kit'; +import { organizationService } from '../../../../modules/organization'; + +export async function load({ params }) { + try { + const organization = await organizationService.findBySlug(params.org); + return { organization }; + } catch { + throw error(404, 'Organization not found'); + } +} diff --git a/src/routes/cluster-settings/orgs/[org]/+page.svelte b/src/routes/cluster-settings/orgs/[org]/+page.svelte new file mode 100644 index 0000000..c0e0ee1 --- /dev/null +++ b/src/routes/cluster-settings/orgs/[org]/+page.svelte @@ -0,0 +1,272 @@ + + + + {organization.name} - Cluster Settings + + +
+ {#if error} +
+ {error} +
+ {/if} + + {#if success} +
+ {success} +
+ {/if} + +
+
+

Información

+ +
+ +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ +
+ +
+
+ +
+
+

Danger Zone

+
+
+
+

Delete organization

+

+ Permanently deletes this organization. This action cannot be undone. +

+
+ +
+
+
+ +{#if deleteModalOpen} + +
+ +
+{/if} From 064fcc4bbb9496fc7d0fa013c6f75c204ce6caf7 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Fri, 21 Aug 2026 13:25:13 +0200 Subject: [PATCH 03/19] organization redirects --- src/lib/components/AppNavbar.svelte | 26 ++++--- src/lib/components/AppSidebar.svelte | 29 +++++--- .../application/organization.service.ts | 13 ++++ src/routes/+layout.server.ts | 7 +- src/routes/+layout.svelte | 5 +- src/routes/+page.server.ts | 5 +- src/routes/org/+page.server.ts | 6 ++ src/routes/org/+page.svelte | 70 +++++++++++++++++++ src/routes/org/[org]/+layout.server.ts | 6 +- src/routes/org/[org]/+page.server.ts | 10 +++ src/routes/settings/projects/+page.server.ts | 3 +- src/routes/settings/projects/+page.svelte | 6 +- 12 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 src/routes/org/+page.server.ts create mode 100644 src/routes/org/+page.svelte create mode 100644 src/routes/org/[org]/+page.server.ts diff --git a/src/lib/components/AppNavbar.svelte b/src/lib/components/AppNavbar.svelte index ba31a4b..a534305 100644 --- a/src/lib/components/AppNavbar.svelte +++ b/src/lib/components/AppNavbar.svelte @@ -6,7 +6,7 @@ export let user: NavbarUser | null = null; export let projects: { id: string; name: string; slug: string }[] = []; - export let organizationSlug = 'gitops'; + export let organizationSlug: string | null = 'gitops'; export let projectSlug = ''; let showUserDropdown = false; @@ -89,7 +89,13 @@
- {#if projects.length === 0} + {#if !organizationSlug} +

+ No organization selected. Choose one. +

+ {:else if projects.length === 0}

No hay proyectos activos.

{:else}
@@ -109,13 +115,15 @@
{/if} - - View all projects - + {#if organizationSlug} + + View all projects + + {/if}
{/if} diff --git a/src/lib/components/AppSidebar.svelte b/src/lib/components/AppSidebar.svelte index 8beba3b..0b21b15 100644 --- a/src/lib/components/AppSidebar.svelte +++ b/src/lib/components/AppSidebar.svelte @@ -31,7 +31,8 @@ export let pathname = '/'; export let isConfigured = false; export let collapsed = false; - export let organizationSlug = 'gitops'; + export let organizationSlug: string | null = null; + export let organizationName: string | null = null; export let projects: { slug: string; modules?: { vault: boolean; openreport: boolean; stateiac: boolean }; @@ -55,13 +56,21 @@ { name: 'Overview', icon: LayoutDashboard, - items: [ - { - label: 'Overview', - href: `/org/${organizationSlug}/overview`, - icon: LayoutDashboard, - }, - ], + items: organizationSlug + ? [ + { + label: 'Overview', + href: `/org/${organizationSlug}/overview`, + icon: LayoutDashboard, + }, + ] + : [ + { + label: 'Seleccionar organización', + href: '/cluster-settings/orgs', + icon: Building2, + }, + ], }, ], }, @@ -187,12 +196,12 @@