Skip to content

Commit

Permalink
Allow minimum password strength to be configurable globally (#168)
Browse files Browse the repository at this point in the history
* clean up settings

* allow changing password strength requirements
  • Loading branch information
cmintey authored Dec 1, 2024
1 parent a0d9ef8 commit 9be08ee
Show file tree
Hide file tree
Showing 18 changed files with 163 additions and 81 deletions.
9 changes: 9 additions & 0 deletions src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ type Config = {
showName: boolean;
};
listMode: ListMode;
security: {
passwordStrength: number;
};
};

type Option = {
Expand All @@ -73,3 +76,9 @@ type GroupInformation = import("@prisma/client").Group & {
isManager: boolean;
active: boolean;
};

type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
12 changes: 3 additions & 9 deletions src/lib/components/PasswordInput.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { zxcvbn, zxcvbnOptions, type ZxcvbnResult } from "@zxcvbn-ts/core";
import { loadOptions } from "$lib/zxcvbn";
import { loadOptions, meterLabel } from "$lib/zxcvbn";
import { popup, ProgressBar, type PopupSettings } from "@skeletonlabs/skeleton";
import { onMount } from "svelte";
Expand Down Expand Up @@ -37,7 +37,7 @@
let strength: ZxcvbnResult | undefined = $derived(value ? zxcvbn(value) : undefined);
let visible = $state(false);
const meterLookup = ["bg-error-500", "bg-error-500", "bg-error-500", "bg-warning-500", "bg-success-500"];
const meterLookup = ["bg-error-500", "bg-error-500", "bg-warning-500", "bg-success-500", "bg-success-500"];
const handleClick = (e: Event) => {
e.preventDefault();
Expand Down Expand Up @@ -113,11 +113,5 @@
</ul>
</div>

{#if strength.score < 3}
<p>Weak</p>
{:else if strength.score < 4}
<p>Moderate</p>
{:else}
<p>Strong</p>
{/if}
<p>{meterLabel[strength?.score]}</p>
{/if}
19 changes: 19 additions & 0 deletions src/lib/components/admin/SettingsForm/BaseSetting.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
title: string;
subtitle?: string;
children: Snippet;
}
const { title, subtitle, children }: Props = $props();
</script>

<div class="flex flex-col space-y-2">
<h2 class="h2">{title}</h2>
{#if subtitle}
<span>{subtitle}</span>
{/if}
{@render children()}
</div>
7 changes: 4 additions & 3 deletions src/lib/components/admin/SettingsForm/Claims.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script lang="ts">
import BaseSetting from "./BaseSetting.svelte";
interface Props {
enabled: boolean;
}
let { enabled = $bindable() }: Props = $props();
</script>

<div class="flex flex-col space-y-2">
<h2 class="h2">Claims</h2>
<BaseSetting title="Claims">
<label class="unstyled flex flex-row space-x-2">
<input id="claimsShowName" name="claimsShowName" class="checkbox" type="checkbox" bind:checked={enabled} />
<span>Show Name</span>
</label>
</div>
</BaseSetting>
6 changes: 3 additions & 3 deletions src/lib/components/admin/SettingsForm/ListMode.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import Alert from "$lib/components/Alert.svelte";
import BaseSetting from "./BaseSetting.svelte";
interface Props {
mode: ListMode;
Expand All @@ -9,8 +10,7 @@
let { mode = $bindable(), disabled = false }: Props = $props();
</script>

<div class="flex flex-col space-y-2">
<h2 class="h2">Wishlist Mode</h2>
<BaseSetting title="Wishlist Mode">
{#if disabled}
<Alert type="info">
There are other members in this group, you cannot switch the mode until you remove all but one member.
Expand All @@ -22,4 +22,4 @@
<option value="registry">Registry</option>
{/if}
</select>
</div>
</BaseSetting>
7 changes: 4 additions & 3 deletions src/lib/components/admin/SettingsForm/PublicSignup.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script lang="ts">
import BaseSetting from "./BaseSetting.svelte";
interface Props {
enabled: boolean;
}
let { enabled = $bindable() }: Props = $props();
</script>

<div class="flex flex-col space-y-2">
<h2 class="h2">Public Signup</h2>
<BaseSetting title="Public Signup">
<label class="unstyled flex flex-row space-x-2">
<input id="enableSignup" name="enableSignup" class="checkbox" type="checkbox" bind:checked={enabled} />
<span>Enable</span>
</label>
</div>
</BaseSetting>
6 changes: 3 additions & 3 deletions src/lib/components/admin/SettingsForm/SMTP.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import PasswordInput from "$lib/components/PasswordInput.svelte";
import BaseSetting from "./BaseSetting.svelte";
interface Props {
enabled: boolean;
Expand All @@ -22,8 +23,7 @@
}: Props = $props();
</script>

<div class="flex flex-col space-y-2">
<h2 class="h2">SMTP</h2>
<BaseSetting title="SMTP">
<label class="unstyled flex flex-row space-x-2">
<input id="enableSMTP" name="enableSMTP" class="checkbox" type="checkbox" bind:checked={enabled} />
<span>Enable</span>
Expand Down Expand Up @@ -93,4 +93,4 @@
</label>
</div>
{/if}
</div>
</BaseSetting>
28 changes: 28 additions & 0 deletions src/lib/components/admin/SettingsForm/Security.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import BaseSetting from "./BaseSetting.svelte";
interface Props {
passwordStrength: number;
}
import { strengthOptions } from "$lib/zxcvbn";
let { passwordStrength = $bindable() }: Props = $props();
</script>

<BaseSetting title="Security">
<label class="flex flex-col" for="passwordStrength">
<span>Password Strength Requirement</span>
<select id="passwordStrength" name="passwordStrength" class="select w-full" bind:value={passwordStrength}>
{#each strengthOptions as label, idx}
<option value={idx - 1}>{label}</option>
{/each}
</select>
<div class="flex flex-row items-center space-x-2">
<iconify-icon icon="ion:information-circle-outline"></iconify-icon>
<span class="font-bold">
Use caution when changing or disabling this value! If disabled, any password of 1 character or more will
be allowed.
</span>
</div>
</label>
</BaseSetting>
11 changes: 6 additions & 5 deletions src/lib/components/admin/SettingsForm/Suggestions.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import BaseSetting from "./BaseSetting.svelte";
interface Props {
enabled: boolean;
method: SuggestionMethod;
Expand All @@ -7,8 +9,7 @@
let { enabled = $bindable(), method = $bindable() }: Props = $props();
</script>

<div class="flex flex-col space-y-2">
<h2 class="h2">Suggestions</h2>
<BaseSetting title="Suggestions">
<label class="unstyled flex flex-row space-x-2">
<input
id="enableSuggestions"
Expand All @@ -20,13 +21,13 @@
<span>Enable</span>
</label>
{#if enabled}
<label class="w-fit">
<label class="flex flex-col" for="suggestionMethod">
<span>Method</span>
<select id="suggestionMethod" name="suggestionMethod" class="select" bind:value={method}>
<select id="suggestionMethod" name="suggestionMethod" class="select w-full" bind:value={method}>
<option value="surprise">Surprise Me</option>
<option value="auto-approval">Auto-Approve</option>
<option value="approval">Approval Required</option>
</select>
</label>
{/if}
</div>
</BaseSetting>
8 changes: 6 additions & 2 deletions src/lib/components/admin/SettingsForm/index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import Suggestions from "./Suggestions.svelte";
import Smtp from "./SMTP.svelte";
import Claims from "./Claims.svelte";
import Security from "./Security.svelte";
interface Props {
config: Config;
Expand All @@ -20,7 +21,7 @@
</script>

<!-- TODO: Add tooltips explaining the various settings -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="grid grid-cols-1 gap-4 gap-y-8 md:grid-cols-2">
<div class="col-span-1">
<PublicSignup bind:enabled={config.enableSignup} />
</div>
Expand All @@ -30,8 +31,11 @@
<div class="col-span-1">
<Claims bind:enabled={config.claims.showName} />
</div>
<div class="col-span-1">
<Security bind:passwordStrength={config.security.passwordStrength} />
</div>

<div class="col-span-1 md:col-span-3">
<div class="col-span-1 md:col-span-2">
<Smtp
bind:enabled={config.smtp.enable}
bind:host={config.smtp.host}
Expand Down
61 changes: 32 additions & 29 deletions src/lib/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { client } from "./prisma";

const GLOBAL = "global";

export enum ConfigKey {
enum ConfigKey {
SIGNUP_ENABLE = "enableSignup",
SUGGESTIONS_ENABLE = "suggestions.enable",
SUGGESTIONS_METHOD = "suggestions.method",
Expand All @@ -14,12 +14,11 @@ export enum ConfigKey {
SMTP_FROM = "smtp.from",
SMTP_FROM_NAME = "smtp.fromName",
CLAIMS_SHOW_NAME = "claims.showName",
LIST_MODE = "listMode"
LIST_MODE = "listMode",
SECURITY_PASSWORD_STRENGTH = "security.passwordStrength"
}

type GroupConfig = Partial<Pick<Config, "suggestions" | "claims" | "listMode">>;

export const getConfig = async (groupId?: string): Promise<Config> => {
export const getConfig = async (groupId?: string, includeSensitive = false): Promise<Config> => {
let configItems = await client.systemConfig.findMany({
where: {
groupId: "global"
Expand All @@ -34,22 +33,26 @@ export const getConfig = async (groupId?: string): Promise<Config> => {
});
}

let groupConfig: GroupConfig = {};
if (groupId) groupConfig = await getGroupConfig(groupId);

const configMap: Record<string, string | null | undefined> = {};
let configMap: Record<string, string | null> = {};
for (const { key, value } of configItems) {
configMap[key] = value;
}

let groupConfigMap: Record<string, string | null> = {};
if (groupId) groupConfigMap = await getGroupConfig(groupId);
configMap = {
...configMap,
...groupConfigMap
};

const smtpConfig: SMTPConfig =
configMap[ConfigKey.SMTP_ENABLE] === "true"
? {
enable: true,
host: configMap[ConfigKey.SMTP_HOST]!,
port: Number(configMap[ConfigKey.SMTP_PORT])!,
user: configMap[ConfigKey.SMTP_USER]!,
pass: configMap[ConfigKey.SMTP_PASS]!,
pass: maskable(configMap[ConfigKey.SMTP_PASS]!, !includeSensitive),
from: configMap[ConfigKey.SMTP_FROM]!,
fromName: configMap[ConfigKey.SMTP_FROM_NAME]!
}
Expand All @@ -58,7 +61,7 @@ export const getConfig = async (groupId?: string): Promise<Config> => {
host: configMap[ConfigKey.SMTP_HOST],
port: Number(configMap[ConfigKey.SMTP_PORT]),
user: configMap[ConfigKey.SMTP_USER],
pass: configMap[ConfigKey.SMTP_PASS],
pass: maskable(configMap[ConfigKey.SMTP_PASS]!, !includeSensitive),
from: configMap[ConfigKey.SMTP_FROM],
fromName: configMap[ConfigKey.SMTP_FROM_NAME]
};
Expand All @@ -67,42 +70,34 @@ export const getConfig = async (groupId?: string): Promise<Config> => {
enableSignup: configMap[ConfigKey.SIGNUP_ENABLE] === "true",
suggestions: {
enable: configMap[ConfigKey.SUGGESTIONS_ENABLE] === "true",
method: (configMap[ConfigKey.SUGGESTIONS_METHOD] as SuggestionMethod) || "approval",
...groupConfig.suggestions
method: (configMap[ConfigKey.SUGGESTIONS_METHOD] as SuggestionMethod) || "approval"
},
smtp: smtpConfig,
claims: {
showName: configMap[ConfigKey.CLAIMS_SHOW_NAME] === "true",
...groupConfig.claims
showName: configMap[ConfigKey.CLAIMS_SHOW_NAME] === "true"
},
listMode: groupConfig.listMode || (configMap[ConfigKey.LIST_MODE] as ListMode) || "standard"
listMode: (configMap[ConfigKey.LIST_MODE] as ListMode) || "standard",
security: {
passwordStrength: Number(configMap[ConfigKey.SECURITY_PASSWORD_STRENGTH] || 2)
}
};

return config;
};

const getGroupConfig = async (groupId: string): Promise<GroupConfig> => {
const getGroupConfig = async (groupId: string): Promise<Record<string, string | null>> => {
const configItems = await client.systemConfig.findMany({
where: {
groupId
}
});

const configMap: Record<string, string | null | undefined> = {};
const configMap: Record<string, string | null> = {};
for (const { key, value } of configItems) {
configMap[key] = value;
}

return {
suggestions: {
enable: configMap[ConfigKey.SUGGESTIONS_ENABLE] === "true",
method: (configMap[ConfigKey.SUGGESTIONS_METHOD] as SuggestionMethod) || "approval"
},
claims: {
showName: configMap[ConfigKey.CLAIMS_SHOW_NAME] === "true"
},
listMode: (configMap[ConfigKey.LIST_MODE] as ListMode) || "standard"
};
return configMap;
};

const createDefaultConfig = async (): Promise<void> => {
Expand All @@ -118,7 +113,10 @@ const createDefaultConfig = async (): Promise<void> => {
claims: {
showName: true
},
listMode: "standard"
listMode: "standard",
security: {
passwordStrength: 2
}
};

await writeConfig(defaultConfig);
Expand All @@ -142,6 +140,7 @@ export const writeConfig = async (config: Partial<Config>, groupId = GLOBAL) =>
configMap[ConfigKey.SUGGESTIONS_METHOD] = config?.suggestions?.method;
configMap[ConfigKey.CLAIMS_SHOW_NAME] = config?.claims?.showName.toString();
configMap[ConfigKey.LIST_MODE] = config?.listMode;
configMap[ConfigKey.SECURITY_PASSWORD_STRENGTH] = config?.security?.passwordStrength.toString();

for (const [key, value] of Object.entries(configMap)) {
await client.systemConfig.upsert({
Expand All @@ -162,3 +161,7 @@ export const writeConfig = async (config: Partial<Config>, groupId = GLOBAL) =>
});
}
};

const maskable = (value: string, mask: boolean) => {
return mask ? "*****" : value;
};
Loading

0 comments on commit 9be08ee

Please sign in to comment.