Add guards against exposing passwords to the UI (#477)

This commit is contained in:
Flaminel
2026-03-02 13:11:34 +02:00
committed by GitHub
parent 41b48d1104
commit bdb956ec84
42 changed files with 1441 additions and 74 deletions

View File

@@ -69,7 +69,7 @@
<app-input label="External URL" placeholder="https://sonarr.example.com" type="url" [(value)]="modalExternalUrl"
hint="Optional URL used in notifications for clickable links (e.g., when internal Docker URLs are not reachable externally)"
helpKey="arr:externalUrl" />
<app-input label="API Key" placeholder="Enter API key" type="password" [(value)]="modalApiKey"
<app-input label="API Key" placeholder="Enter API key" type="password" [revealable]="false" [(value)]="modalApiKey"
hint="API key from your arr application's Settings > General"
[error]="modalApiKeyError()"
helpKey="arr:apiKey" />

View File

@@ -146,6 +146,7 @@ export class ArrSettingsComponent implements HasPendingChanges {
url: this.modalUrl(),
apiKey: this.modalApiKey(),
version: (this.modalVersion() as number) ?? 3,
instanceId: this.editingInstance()?.id,
};
this.testing.set(true);
this.api.testInstance(this.arrType() as ArrType, request).subscribe({

View File

@@ -77,7 +77,7 @@
helpKey="download-client:username" />
}
@if (showPasswordField()) {
<app-input label="Password" placeholder="Enter password" type="password" [(value)]="modalPassword"
<app-input label="Password" placeholder="Enter password" type="password" [revealable]="false" [(value)]="modalPassword"
[hint]="passwordHint()"
helpKey="download-client:password" />
}

View File

@@ -170,6 +170,7 @@ export class DownloadClientsComponent implements OnInit, HasPendingChanges {
username: this.modalUsername(),
password: this.modalPassword(),
urlBase: this.modalUrlBase(),
clientId: this.editingClient()?.id,
};
this.testing.set(true);
this.api.test(request).subscribe({

View File

@@ -103,7 +103,7 @@
<!-- Discord Fields -->
@if (modalType() === 'Discord') {
<app-input label="Webhook URL" placeholder="https://discord.com/api/webhooks/..." type="password" [(value)]="modalWebhookUrl"
<app-input label="Webhook URL" placeholder="https://discord.com/api/webhooks/..." type="password" [revealable]="false" [(value)]="modalWebhookUrl"
hint="Your Discord webhook URL. Create one in your Discord server's channel settings under Integrations."
[error]="discordWebhookError()"
helpKey="notifications/discord:webhookUrl" />
@@ -117,7 +117,7 @@
<!-- Telegram Fields -->
@if (modalType() === 'Telegram') {
<app-input label="Bot Token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" type="password" [(value)]="modalBotToken"
<app-input label="Bot Token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" type="password" [revealable]="false" [(value)]="modalBotToken"
hint="Create a bot with BotFather and paste the API token"
[error]="telegramBotTokenError()"
helpKey="notifications/telegram:botToken" />
@@ -135,7 +135,7 @@
<!-- Notifiarr Fields -->
@if (modalType() === 'Notifiarr') {
<app-input label="API Key" placeholder="Enter API key" type="password" [(value)]="modalApiKey"
<app-input label="API Key" placeholder="Enter API key" type="password" [revealable]="false" [(value)]="modalApiKey"
hint="Your Notifiarr API key from your dashboard. Requires Passthrough integration."
[error]="notifiarrApiKeyError()"
helpKey="notifications/notifiarr:apiKey" />
@@ -187,12 +187,12 @@
<app-input label="Username" placeholder="Enter username" [(value)]="modalNtfyUsername"
hint="Your username for basic authentication."
helpKey="notifications/ntfy:username" />
<app-input label="Password" placeholder="Enter password" type="password" [(value)]="modalNtfyPassword"
<app-input label="Password" placeholder="Enter password" type="password" [revealable]="false" [(value)]="modalNtfyPassword"
hint="Your password for basic authentication."
helpKey="notifications/ntfy:password" />
}
@if (modalNtfyAuthType() === 'AccessToken') {
<app-input label="Access Token" placeholder="Enter access token" type="password" [(value)]="modalNtfyAccessToken"
<app-input label="Access Token" placeholder="Enter access token" type="password" [revealable]="false" [(value)]="modalNtfyAccessToken"
hint="Your access token for bearer token authentication."
helpKey="notifications/ntfy:accessToken" />
}
@@ -206,11 +206,11 @@
<!-- Pushover Fields -->
@if (modalType() === 'Pushover') {
<app-input label="API Token" placeholder="Enter API token" type="password" [(value)]="modalPushoverApiToken"
<app-input label="API Token" placeholder="Enter API token" type="password" [revealable]="false" [(value)]="modalPushoverApiToken"
hint="Your application API token from Pushover. Create one at pushover.net/apps/build."
[error]="pushoverApiTokenError()"
helpKey="notifications/pushover:apiToken" />
<app-input label="User Key" placeholder="Enter user key" type="password" [(value)]="modalPushoverUserKey"
<app-input label="User Key" placeholder="Enter user key" type="password" [revealable]="false" [(value)]="modalPushoverUserKey"
hint="Your user/group key from your Pushover dashboard."
[error]="pushoverUserKeyError()"
helpKey="notifications/pushover:userKey" />
@@ -247,7 +247,7 @@
hint="The base URL of your Gotify server instance."
[error]="gotifyServerUrlError()"
helpKey="notifications/gotify:serverUrl" />
<app-input label="Application Token" placeholder="Enter application token" type="password" [(value)]="modalGotifyApplicationToken"
<app-input label="Application Token" placeholder="Enter application token" type="password" [revealable]="false" [(value)]="modalGotifyApplicationToken"
hint="The application token from your Gotify server. Create one under Apps in the Gotify web UI."
[error]="gotifyApplicationTokenError()"
helpKey="notifications/gotify:applicationToken" />

View File

@@ -478,6 +478,7 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
testNotification(): void {
const type = this.modalType();
this.testing.set(true);
const providerId = this.editingProvider()?.id;
switch (type) {
case NotificationProviderType.Discord:
@@ -485,6 +486,7 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
webhookUrl: this.modalWebhookUrl(),
username: this.modalUsername() || undefined,
avatarUrl: this.modalAvatarUrl() || undefined,
providerId,
}).subscribe({
next: (r) => { this.toast.success(r.message || 'Test sent'); this.testing.set(false); },
error: () => { this.toast.error('Test failed'); this.testing.set(false); },
@@ -496,6 +498,7 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
chatId: this.modalChatId(),
topicId: this.modalTopicId() || undefined,
sendSilently: this.modalSendSilently(),
providerId,
}).subscribe({
next: (r) => { this.toast.success(r.message || 'Test sent'); this.testing.set(false); },
error: () => { this.toast.error('Test failed'); this.testing.set(false); },
@@ -505,6 +508,7 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
this.api.testNotifiarr({
apiKey: this.modalApiKey(),
channelId: this.modalChannelId(),
providerId,
}).subscribe({
next: (r) => { this.toast.success(r.message || 'Test sent'); this.testing.set(false); },
error: () => { this.toast.error('Test failed'); this.testing.set(false); },
@@ -517,6 +521,7 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
key: this.modalAppriseKey() || undefined,
tags: this.modalAppriseTags() || undefined,
serviceUrls: this.modalAppriseServiceUrls().join('\n') || undefined,
providerId,
}).subscribe({
next: (r) => { this.toast.success(r.message || 'Test sent'); this.testing.set(false); },
error: () => { this.toast.error('Test failed'); this.testing.set(false); },
@@ -532,6 +537,7 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
accessToken: this.modalNtfyAccessToken() || undefined,
priority: this.modalNtfyPriority() as NtfyPriority,
tags: this.modalNtfyTags().length > 0 ? this.modalNtfyTags() : undefined,
providerId,
}).subscribe({
next: (r) => { this.toast.success(r.message || 'Test sent'); this.testing.set(false); },
error: () => { this.toast.error('Test failed'); this.testing.set(false); },
@@ -548,6 +554,7 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
retry: this.modalPushoverPriority() === PushoverPriority.Emergency ? (this.modalPushoverRetry() ?? 30) : undefined,
expire: this.modalPushoverPriority() === PushoverPriority.Emergency ? (this.modalPushoverExpire() ?? 3600) : undefined,
tags: this.modalPushoverTags().length > 0 ? this.modalPushoverTags() : undefined,
providerId,
}).subscribe({
next: (r) => { this.toast.success(r.message || 'Test sent'); this.testing.set(false); },
error: () => { this.toast.error('Test failed'); this.testing.set(false); },
@@ -559,6 +566,7 @@ export class NotificationsComponent implements OnInit, HasPendingChanges {
serverUrl: this.modalGotifyServerUrl(),
applicationToken: this.modalGotifyApplicationToken(),
priority: parseInt(this.modalGotifyPriority() as string, 10) || 5,
providerId,
}).subscribe({
next: (r) => { this.toast.success(r.message || 'Test sent'); this.testing.set(false); },
error: () => { this.toast.error('Test failed'); this.testing.set(false); },

View File

@@ -26,4 +26,5 @@ export interface TestArrInstanceRequest {
url: string;
apiKey: string;
version: number;
instanceId?: string;
}

View File

@@ -36,6 +36,7 @@ export interface TestDownloadClientRequest {
username?: string;
password?: string;
urlBase?: string;
clientId?: string;
}
export interface TestConnectionResult {

View File

@@ -150,6 +150,7 @@ export interface CreateGotifyProviderRequest {
export interface TestNotifiarrRequest {
apiKey: string;
channelId: string;
providerId?: string;
}
export interface TestAppriseRequest {
@@ -158,6 +159,7 @@ export interface TestAppriseRequest {
key?: string;
tags?: string;
serviceUrls?: string;
providerId?: string;
}
export interface TestNtfyRequest {
@@ -169,6 +171,7 @@ export interface TestNtfyRequest {
accessToken?: string;
priority: NtfyPriority;
tags?: string[];
providerId?: string;
}
export interface TestTelegramRequest {
@@ -176,12 +179,14 @@ export interface TestTelegramRequest {
chatId: string;
topicId?: string;
sendSilently: boolean;
providerId?: string;
}
export interface TestDiscordRequest {
webhookUrl: string;
username?: string;
avatarUrl?: string;
providerId?: string;
}
export interface TestPushoverRequest {
@@ -193,12 +198,14 @@ export interface TestPushoverRequest {
retry?: number;
expire?: number;
tags?: string[];
providerId?: string;
}
export interface TestGotifyRequest {
serverUrl: string;
applicationToken: string;
priority: number;
providerId?: string;
}
export interface TestNotificationResult {

View File

@@ -13,7 +13,7 @@
#inputEl
class="input-field"
[class.input-field--error]="error()"
[class.input-field--has-eye]="type() === 'password'"
[class.input-field--has-eye]="hasEye()"
[type]="effectiveType()"
[placeholder]="placeholder()"
[disabled]="disabled()"
@@ -21,7 +21,7 @@
[(ngModel)]="value"
(blur)="blurred.emit($event)"
/>
@if (type() === 'password') {
@if (hasEye()) {
<button type="button" class="input-eye-btn" (click)="toggleSecret($event)" [attr.aria-label]="showSecret() ? 'Hide password' : 'Show password'">
<ng-icon [name]="showSecret() ? 'tablerEyeOff' : 'tablerEye'" size="16" />
</button>

View File

@@ -19,6 +19,7 @@ export class InputComponent {
type = input<'text' | 'password' | 'email' | 'url' | 'search' | 'datetime-local' | 'date' | 'number'>('text');
disabled = input(false);
readonly = input(false);
revealable = input(true);
error = input<string>();
hint = input<string>();
helpKey = input<string>();
@@ -29,8 +30,9 @@ export class InputComponent {
blurred = output<FocusEvent>();
readonly showSecret = signal(false);
readonly hasEye = computed(() => this.type() === 'password' && this.revealable());
readonly effectiveType = computed(() => {
if (this.type() === 'password' && this.showSecret()) return 'text';
if (this.hasEye() && this.showSecret()) return 'text';
return this.type();
});
@@ -40,6 +42,7 @@ export class InputComponent {
toggleSecret(event: Event): void {
event.preventDefault();
if (!this.revealable()) return;
this.showSecret.update(v => !v);
}