Skip to content

Commit 053435f

Browse files
authored
Merge pull request #34 from encryption4all/feat/admin-org-management
feat(admin): create/delete organizations + fix header toggle layout
2 parents e5b41a5 + 12dd3a2 commit 053435f

20 files changed

Lines changed: 1063 additions & 170 deletions

File tree

src/lib/assets/images/logo-dark.svg

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/lib/assets/images/logo-wide-dark.svg

Lines changed: 1 addition & 0 deletions
Loading

src/lib/assets/images/logo-wide.svg

Lines changed: 1 addition & 0 deletions
Loading

src/lib/assets/images/logo.svg

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/lib/components/Header.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import ThemeSwitcher from './ThemeSwitcher.svelte';
44
import LocaleSwitcher from './LocaleSwitcher.svelte';
55
import Icon from '@iconify/svelte';
6-
import logoLight from '$lib/assets/images/logo.svg';
7-
import logoDark from '$lib/assets/images/logo-dark.svg';
6+
import logoLight from '$lib/assets/images/logo-wide.svg';
7+
import logoDark from '$lib/assets/images/logo-wide-dark.svg';
88
99
type AuthProp =
1010
| { loggedIn: true; email: string | null; portalHref: string }

src/lib/global.scss

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,16 @@ body {
5050
--pg-font-weight-bold: 700;
5151
--pg-font-weight-extrabold: 800;
5252

53-
/* Colors — Purple business colorway */
54-
--pg-primary: #7c3aed;
55-
--pg-primary-contrast: #6d28d9;
56-
/* Background shade used wherever white text sits on purple (buttons, badges). */
53+
/* Colors — Navy business colorway */
54+
--pg-primary: #1e40af;
55+
--pg-primary-contrast: #1e3a8a;
56+
/* Background shade used wherever white text sits on navy (buttons, badges). */
5757
/* Must keep contrast >= 4.5:1 against white in both themes. */
58-
--pg-primary-bg: #7c3aed; /* white = 4.76:1, passes AA */
59-
--pg-primary-bg-hover: #6d28d9; /* white = 5.84:1 */
58+
--pg-primary-bg: #1e40af; /* white = 8.59:1, passes AAA */
59+
--pg-primary-bg-hover: #1e3a8a; /* white = 10.36:1 */
6060
--pg-general-background: #ffffff;
61-
--pg-soft-background: #f5f3ff;
62-
--pg-strong-background: #ddd6fe;
61+
--pg-soft-background: #eff6ff;
62+
--pg-strong-background: #bfdbfe;
6363
--pg-text: #030e17;
6464
--pg-text-secondary: #4d6070;
6565
--pg-input-normal: #4d6070;
@@ -74,15 +74,15 @@ body {
7474
}
7575

7676
.dark {
77-
--pg-primary: #a78bfa;
78-
--pg-primary-contrast: #6d28d9;
79-
/* Keep the darker purple in dark mode too so white-text-on-purple */
77+
--pg-primary: #93c5fd;
78+
--pg-primary-contrast: #1e3a8a;
79+
/* Keep a darker navy in dark mode too so white-text-on-navy */
8080
/* buttons / badges keep a passing contrast ratio. */
81-
--pg-primary-bg: #7c3aed;
82-
--pg-primary-bg-hover: #6d28d9;
83-
--pg-general-background: #0f0a1e;
84-
--pg-soft-background: #1a1030;
85-
--pg-strong-background: #2d1b69;
81+
--pg-primary-bg: #1e40af;
82+
--pg-primary-bg-hover: #1e3a8a;
83+
--pg-general-background: #0a1428;
84+
--pg-soft-background: #0f1e3d;
85+
--pg-strong-background: #1e3a6e;
8686
--pg-text: #ffffff;
8787
--pg-text-secondary: #92a3af;
8888
--pg-input-normal: #92a3af;

src/lib/locales/en.json

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,49 @@
316316
"empty": "(empty)",
317317
"validationNotes": "Validation notes (what did you check?)",
318318
"approve": "Approve",
319-
"reject": "Reject"
319+
"reject": "Reject",
320+
"delete": "Delete",
321+
"deleteConfirm": "Delete organization",
322+
"deleteConfirmTitle": "Delete this organization?",
323+
"deleteConfirmIntro": "This will permanently delete \"{name}\" and all its users, API keys, change requests, email logs, and DNS verifications.",
324+
"deleteConfirmWarning": "This action cannot be undone.",
325+
"deleteConfirmLabel": "Type the organization name shown below to confirm",
326+
"cancel": "Cancel",
327+
"deleteSuccess": "Organization \"{name}\" has been deleted.",
328+
"deleteFailedNameMismatch": "The name you entered did not match. Deletion cancelled.",
329+
"suspendConfirmTitle": "Suspend this organization?",
330+
"suspendConfirmIntro": "{name} will be blocked from using the service. You can reactivate it later.",
331+
"suspendConfirmAction": "Suspend organisation",
332+
"activateConfirmTitle": "Activate this organization?",
333+
"activateConfirmIntro": "{name} will be marked as active and able to use the service.",
334+
"activateConfirmAction": "Activate organisation",
335+
"reactivateConfirmTitle": "Reactivate this organization?",
336+
"reactivateConfirmIntro": "{name} will be marked as active again.",
337+
"reactivateConfirmAction": "Reactivate organisation",
338+
"statusChangedTo_active": "Organization is now active.",
339+
"statusChangedTo_suspended": "Organization is now suspended.",
340+
"addOrg": "Add organization",
341+
"createSubmit": "Create organization",
342+
"createSuccess": "Organization \"{name}\" has been created.",
343+
"create_required_name": "Organization name is required.",
344+
"create_required_domain": "Domain is required.",
345+
"create_required_email": "Signing email is required.",
346+
"create_invalid_domain": "Invalid domain (e.g. example.com).",
347+
"create_invalid_email": "Invalid email address.",
348+
"create_duplicate_domain": "An organization with this domain already exists.",
349+
"create_unexpected": "An unexpected error occurred. Please try again.",
350+
"usersTitle": "Users",
351+
"addUser": "Add user",
352+
"addUserSubmit": "Add user",
353+
"userFullName": "Full name",
354+
"userEmail": "Email",
355+
"userPhone": "Phone",
356+
"addUserSuccess": "User \"{name}\" has been added.",
357+
"addUser_required_name": "Full name is required.",
358+
"addUser_required_email": "Email is required.",
359+
"addUser_invalid_email": "Invalid email address.",
360+
"addUser_duplicate_email": "A user with this email already exists.",
361+
"addUser_unexpected": "An unexpected error occurred. Please try again."
320362
},
321363
"apiKeys": {
322364
"title": "All API Keys",

src/lib/locales/nl.json

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,49 @@
316316
"empty": "(leeg)",
317317
"validationNotes": "Validatienotities (wat heeft u gecontroleerd?)",
318318
"approve": "Goedkeuren",
319-
"reject": "Afwijzen"
319+
"reject": "Afwijzen",
320+
"delete": "Verwijderen",
321+
"deleteConfirm": "Organisatie verwijderen",
322+
"deleteConfirmTitle": "Deze organisatie verwijderen?",
323+
"deleteConfirmIntro": "Hiermee verwijdert u \"{name}\" definitief, inclusief alle gebruikers, API-sleutels, wijzigingsverzoeken, e-maillogs en DNS-verificaties.",
324+
"deleteConfirmWarning": "Deze actie kan niet ongedaan worden gemaakt.",
325+
"deleteConfirmLabel": "Typ de hieronder getoonde organisatienaam ter bevestiging",
326+
"cancel": "Annuleren",
327+
"deleteSuccess": "Organisatie \"{name}\" is verwijderd.",
328+
"deleteFailedNameMismatch": "De ingevoerde naam komt niet overeen. Verwijdering geannuleerd.",
329+
"suspendConfirmTitle": "Deze organisatie opschorten?",
330+
"suspendConfirmIntro": "{name} kan de dienst niet meer gebruiken. U kunt de organisatie later weer heractiveren.",
331+
"suspendConfirmAction": "Organisatie opschorten",
332+
"activateConfirmTitle": "Deze organisatie activeren?",
333+
"activateConfirmIntro": "{name} wordt als actief gemarkeerd en kan de dienst gebruiken.",
334+
"activateConfirmAction": "Organisatie activeren",
335+
"reactivateConfirmTitle": "Deze organisatie heractiveren?",
336+
"reactivateConfirmIntro": "{name} wordt opnieuw als actief gemarkeerd.",
337+
"reactivateConfirmAction": "Organisatie heractiveren",
338+
"statusChangedTo_active": "Organisatie is nu actief.",
339+
"statusChangedTo_suspended": "Organisatie is nu opgeschort.",
340+
"addOrg": "Organisatie toevoegen",
341+
"createSubmit": "Organisatie aanmaken",
342+
"createSuccess": "Organisatie \"{name}\" is aangemaakt.",
343+
"create_required_name": "Organisatienaam is verplicht.",
344+
"create_required_domain": "Domein is verplicht.",
345+
"create_required_email": "Ondertekenings-e-mail is verplicht.",
346+
"create_invalid_domain": "Ongeldig domein (bijv. example.com).",
347+
"create_invalid_email": "Ongeldig e-mailadres.",
348+
"create_duplicate_domain": "Er bestaat al een organisatie met dit domein.",
349+
"create_unexpected": "Er is een onverwachte fout opgetreden. Probeer het opnieuw.",
350+
"usersTitle": "Gebruikers",
351+
"addUser": "Gebruiker toevoegen",
352+
"addUserSubmit": "Gebruiker toevoegen",
353+
"userFullName": "Volledige naam",
354+
"userEmail": "E-mailadres",
355+
"userPhone": "Telefoonnummer",
356+
"addUserSuccess": "Gebruiker \"{name}\" is toegevoegd.",
357+
"addUser_required_name": "Volledige naam is verplicht.",
358+
"addUser_required_email": "E-mailadres is verplicht.",
359+
"addUser_invalid_email": "Ongeldig e-mailadres.",
360+
"addUser_duplicate_email": "Er bestaat al een gebruiker met dit e-mailadres.",
361+
"addUser_unexpected": "Er is een onverwachte fout opgetreden. Probeer het opnieuw."
320362
},
321363
"apiKeys": {
322364
"title": "Alle API-sleutels",

src/lib/server/services/admin.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
changeRequests,
66
adminAuditLog,
77
apiKeys,
8-
adminAccounts
8+
adminAccounts,
9+
sessions
910
} from '$lib/server/db/schema';
1011
import { eq, desc, and, isNull, or, count, sql } from 'drizzle-orm';
1112

@@ -147,6 +148,58 @@ export async function suspendOrganization(orgId: string) {
147148
.where(eq(organizations.id, orgId));
148149
}
149150

151+
export async function getOrganizationById(orgId: string) {
152+
const rows = await db
153+
.select()
154+
.from(organizations)
155+
.where(eq(organizations.id, orgId))
156+
.limit(1);
157+
return rows[0] ?? null;
158+
}
159+
160+
export async function deleteOrganization(orgId: string) {
161+
await db
162+
.delete(sessions)
163+
.where(or(eq(sessions.orgId, orgId), eq(sessions.impersonatingOrgId, orgId)));
164+
await db.delete(organizations).where(eq(organizations.id, orgId));
165+
}
166+
167+
export async function createOrganization(input: {
168+
name: string;
169+
domain: string;
170+
signingEmail: string;
171+
kvkNumber: string | null;
172+
status: 'active' | 'pending' | 'suspended';
173+
}) {
174+
const [org] = await db
175+
.insert(organizations)
176+
.values({
177+
name: input.name,
178+
domain: input.domain,
179+
signingEmail: input.signingEmail,
180+
kvkNumber: input.kvkNumber,
181+
status: input.status
182+
})
183+
.returning();
184+
return org;
185+
}
186+
187+
export async function addUserToOrganization(
188+
orgId: string,
189+
input: { email: string; fullName: string; phone: string | null }
190+
) {
191+
const [user] = await db
192+
.insert(users)
193+
.values({
194+
email: input.email,
195+
fullName: input.fullName,
196+
phone: input.phone,
197+
orgId
198+
})
199+
.returning();
200+
return user;
201+
}
202+
150203
export async function listAdminAuditLog(page: number = 1) {
151204
const pageSize = 50;
152205
const offset = (page - 1) * pageSize;

src/routes/(admin)/+layout.svelte

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script lang="ts">
22
import { _ } from 'svelte-i18n';
33
import Icon from '@iconify/svelte';
4-
import logoLight from '$lib/assets/images/logo.svg';
5-
import logoDark from '$lib/assets/images/logo-dark.svg';
4+
import logoLight from '$lib/assets/images/logo-wide.svg';
5+
import logoDark from '$lib/assets/images/logo-wide-dark.svg';
66
import ThemeSwitcher from '$lib/components/ThemeSwitcher.svelte';
77
import LocaleSwitcher from '$lib/components/LocaleSwitcher.svelte';
88
import type { LayoutData } from './$types';
@@ -243,6 +243,9 @@
243243
244244
.header-actions {
245245
margin-left: auto;
246+
display: flex;
247+
align-items: center;
248+
gap: 0.5rem;
246249
}
247250
248251
.header-title { font-size: var(--pg-font-size-lg); margin: 0; }

0 commit comments

Comments
 (0)