1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-14 18:12:31 +00:00

feat(signup): add default user groups and claims for new users (#812)

Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Zeedif
2025-08-22 06:25:02 -06:00
committed by GitHub
parent c51265dafb
commit 182d809028
18 changed files with 735 additions and 308 deletions

View File

@@ -0,0 +1,140 @@
<script lang="ts">
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import * as Command from '$lib/components/ui/command';
import * as Popover from '$lib/components/ui/popover';
import { cn } from '$lib/utils/style';
import { LoaderCircle, LucideCheck, LucideChevronDown } from '@lucide/svelte';
import type { FormEventHandler } from 'svelte/elements';
type Item = {
value: string;
label: string;
};
let {
items,
selectedItems = $bindable(),
onSelect,
oninput,
isLoading = false,
placeholder = 'Select items...',
searchText = 'Search...',
noItemsText = 'No items found.',
disableInternalSearch = false,
id
}: {
items: Item[];
selectedItems: string[];
onSelect?: (value: string[]) => void;
oninput?: FormEventHandler<HTMLInputElement>;
isLoading?: boolean;
placeholder?: string;
searchText?: string;
noItemsText?: string;
disableInternalSearch?: boolean;
id?: string;
} = $props();
let open = $state(false);
let searchValue = $state('');
let filteredItems = $state(items);
const selectedLabels = $derived(
items.filter((item) => selectedItems.includes(item.value)).map((item) => item.label)
);
function handleItemSelect(value: string) {
let newSelectedItems: string[];
if (selectedItems.includes(value)) {
newSelectedItems = selectedItems.filter((item) => item !== value);
} else {
newSelectedItems = [...selectedItems, value];
}
selectedItems = newSelectedItems;
onSelect?.(newSelectedItems);
}
function filterItems(search: string) {
if (disableInternalSearch) return;
searchValue = search;
if (!search) {
filteredItems = items;
} else {
filteredItems = items.filter((item) =>
item.label.toLowerCase().includes(search.toLowerCase())
);
}
}
// Reset search value when the popover is closed
$effect(() => {
if (!open) {
filterItems('');
}
filteredItems = items;
});
</script>
<Popover.Root bind:open>
<Popover.Trigger {id}>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
role="combobox"
aria-expanded={open}
class="h-auto min-h-10 w-full justify-between"
>
<div class="flex flex-wrap items-center gap-1">
{#if selectedItems.length > 0}
{#each selectedLabels as label}
<Badge variant="secondary">{label}</Badge>
{/each}
{:else}
<span class="text-muted-foreground font-normal">{placeholder}</span>
{/if}
</div>
<LucideChevronDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="p-0" sameWidth>
<Command.Root shouldFilter={false}>
<Command.Input
placeholder={searchText}
value={searchValue}
oninput={(e) => {
filterItems(e.currentTarget.value);
oninput?.(e);
}}
/>
<Command.Empty>
{#if isLoading}
<div class="flex w-full items-center justify-center py-2">
<LoaderCircle class="size-4 animate-spin" />
</div>
{:else}
{noItemsText}
{/if}
</Command.Empty>
<Command.Group class="max-h-60 overflow-y-auto">
{#each filteredItems as item}
<Command.Item
aria-checked={selectedItems.includes(item.value)}
value={item.value}
onSelect={() => {
handleItemSelect(item.value);
}}
>
<LucideCheck
class={cn('mr-2 size-4', !selectedItems.includes(item.value) && 'text-transparent')}
/>
{item.label}
</Command.Item>
{/each}
</Command.Group>
</Command.Root>
</Popover.Content>
</Popover.Root>

View File

@@ -14,10 +14,15 @@ export default class AppConfigService extends APIService {
}
async update(appConfig: AllAppConfig) {
// Convert all values to string
const appConfigConvertedToString = {};
// Convert all values to string, stringifying JSON where needed
const appConfigConvertedToString: Record<string, string> = {};
for (const key in appConfig) {
(appConfigConvertedToString as any)[key] = (appConfig as any)[key].toString();
const value = (appConfig as any)[key];
if (typeof value === 'object' && value !== null) {
appConfigConvertedToString[key] = JSON.stringify(value);
} else {
appConfigConvertedToString[key] = String(value);
}
}
const res = await this.api.put('/application-configuration', appConfigConvertedToString);
return this.parseConfigList(res.data);
@@ -66,6 +71,16 @@ export default class AppConfigService extends APIService {
}
private parseValue(value: string) {
// Try to parse JSON first
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) {
return parsed;
}
value = String(parsed);
} catch {}
// Handle rest of the types
if (value === 'true') {
return true;
} else if (value === 'false') {

View File

@@ -1,3 +1,5 @@
import type { CustomClaim } from './custom-claim.type';
export type AppConfig = {
appName: string;
allowOwnAccountEdit: boolean;
@@ -14,6 +16,8 @@ export type AllAppConfig = AppConfig & {
// General
sessionDuration: number;
emailsVerified: boolean;
signupDefaultUserGroupIDs: string[];
signupDefaultCustomClaims: CustomClaim[];
// Email
smtpHost: string;
smtpPort: number;