Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3213ed3
feat(auth): stop persisting the plaintext login password (Unit 1)
mairas Jun 24, 2026
06f2833
feat(auth): synchronous auth-mode detection + cookie-mode token suppr…
mairas Jun 24, 2026
fc057ce
feat(auth): derive cookie-mode session state from loginStatus
mairas Jun 24, 2026
0f96e0b
feat(auth): carry the session cookie on REST, WS and the Freeboard if…
mairas Jun 24, 2026
42240f9
feat(auth): route config persistence by resolved mode, not useSharedC…
mairas Jun 24, 2026
34f1df4
feat(auth): cookie-mode SSO bootstrap redirect with budget and recovery
mairas Jun 24, 2026
0798893
feat(auth): cookie-mode Connectivity UX and SSO login route
mairas Jun 24, 2026
7cfc8bf
docs(auth): add CHANGELOG entry; mark Units 6-7-9 complete
mairas Jun 24, 2026
c3d8303
fix(auth): gate write capability on userLevel, not the server readOnl…
mairas Jun 24, 2026
6d3437c
fix(auth): bound loginStatus with a timeout; drop dead renewal/readOn…
mairas Jun 24, 2026
8cc428d
fix(storage): timeout getConfig and survive a failed patch
mairas Jun 24, 2026
186c80f
fix(auth): scope cookie-mode withCredentials to same-origin requests
mairas Jun 24, 2026
503d604
fix(bootstrap): do not open an anonymous WebSocket on the auth-blocke…
mairas Jun 24, 2026
d8e6603
fix(settings): guard empty server config; simplify useDeviceToken
mairas Jun 24, 2026
27d46e4
fix(connectivity): persist config only after the token-mode login suc…
mairas Jun 24, 2026
08b3b57
fix(auth): reject a returnTo that normalizes to a protocol-relative path
mairas Jun 24, 2026
2e7ecb8
test: align global auth/bootstrap stubs with the new public surface
mairas Jun 24, 2026
1571183
feat(settings): runtime-mutable active profile + storage write-safety…
mairas Jun 23, 2026
8ba20fa
feat(profiles): add ProfileService for profile CRUD + switching (Unit 3)
mairas Jun 23, 2026
f88d816
feat(settings): hoist remote-control identity to per-device config (U…
mairas Jun 23, 2026
0fbae43
feat(profiles): Profiles UI in the Configurations tab (Unit 4)
mairas Jun 23, 2026
3c87b51
fix(profiles): gate availability on the user session, not token presence
mairas Jun 24, 2026
693518f
feat(profiles): collapse profile creation to a single "New" action
mairas Jun 24, 2026
7a4aa42
fix(profiles): harden ProfileService mutations (review #2)
mairas Jun 25, 2026
081e3f7
fix(profiles): gate profile write controls on canWriteUserData (revie…
mairas Jun 25, 2026
b1b4475
fix(config): preserve connectionConfig version across writes (review #2)
mairas Jun 25, 2026
703c8be
fix(storage): make awaitQueueDrain report patch failures (review #2)
mairas Jun 25, 2026
2a86ffe
fix(profiles): theme the profile-row divider; cover the demo-config g…
mairas Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Unreleased
## New Features
* Same-origin Signal K sign-in (SSO): when KIP is served by a Signal K server on the same origin, it authenticates with the server's session cookie instead of its own username/password form. With an OIDC/SSO server, KIP joins the existing session and redirects to the server login when needed — no second sign-in, and OIDC-provisioned users (who have no server-local password) can use KIP. Cross-origin standalone use (a PWA pointed at a remote server) keeps the existing token sign-in.
## Improvements
* The Connectivity settings show your Signal K session identity in same-origin mode (including a read-only-session indicator) instead of a credential form.
* Security: the Signal K login password is no longer stored in the browser; it is used only in memory to obtain a session token.
## Fixes
* OIDC/SSO users could not sign in to KIP because it required a server-local password they do not have; same-origin SSO mode resolves this.
## Behavior changes
* Cross-origin token sign-in: the Signal K server provides no token-refresh endpoint, so the stored password is no longer re-sent to renew a session. The session token still persists across browser reloads until it expires; when it expires you are prompted to sign in again.

# v4.8.0
## New Features
* Solar Charger Widget: Get instant clarity on your solar system with a compact, purpose-built Solar Charger Widget. Track individual panels or full arrays in real time, including State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature. Device discovery is automatic, and Zones support keeps warnings and alarms state highly visible.
Expand Down
20 changes: 20 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ConfigurationUpgradeService } from './core/services/configuration-upgra
import { RemoteDashboardsService } from './core/services/remote-dashboards.service';
import { ToastService } from './core/services/toast.service';
import { AppNetworkInitService, IBootstrapIssue } from './core/services/app-initNetwork.service';
import { SsoRedirectService } from './core/services/sso-redirect.service';
import { DashboardHistorySeriesSyncService } from './core/services/dashboard-history-series-sync.service';

@Component({
Expand All @@ -46,6 +47,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly _deltaService = inject(SignalKDeltaService);
private readonly _connectionStateMachine = inject(ConnectionStateMachine);
private readonly _appNetworkInit = inject(AppNetworkInitService);
private readonly _ssoRedirect = inject(SsoRedirectService);
private readonly _historySeriesReconcile = inject(DashboardHistorySeriesSyncService);
public readonly authenticationService = inject(AuthenticationService);
private readonly _dataSet = inject(DatasetStreamService);
Expand Down Expand Up @@ -82,6 +84,7 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
private scheduledOpen: number | null = null;
private readonly OPEN_DELAY_MS = 300; // should match/ exceed sidenav close animation time
private missingConfigPromptShown = false;
private authBlockedPromptShown = false;

// Stable handler refs (prevent leak from rebinding)
private readonly _swipeLeftHandler = () => this.onSwipeLeft();
Expand Down Expand Up @@ -227,6 +230,23 @@ export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
}
});
});

// Cookie-mode auth blocked (SSO auto-login looped out of budget, or sign-in required): offer an
// explicit Sign in that resets the budget and disables auto-login so it is not auto-bounced.
effect(() => {
const issue = this.bootstrapIssue();
if (issue.reason !== 'auth-blocked' || this.authBlockedPromptShown) {
return;
}
this.authBlockedPromptShown = true;
const message = issue.cause === 'budget-exhausted'
? 'Automatic sign-in did not complete. Sign in to Signal K to continue.'
: 'Sign in to Signal K to access your configuration.';
const ref = this.toast.show(message, 0, true, 'warn', 'Sign in');
ref.onAction()
.pipe(takeUntilDestroyed(this._destroyRef))
.subscribe(() => this._ssoRedirect.manualSignIn());
});
}

private isUrlDashboard(url: string | null | undefined): boolean {
Expand Down
267 changes: 68 additions & 199 deletions src/app/core/components/options/configuration/config.component.html
Original file line number Diff line number Diff line change
@@ -1,237 +1,106 @@
<div class="page-content">
<p>
Recommended Reading: To understand configuration management and storage
options, consult the
A profile is a named set of dashboards, layouts and theme. Switching is remembered
<strong>on this device only</strong>, so each display can show a different profile from the same
Signal K login. See the
<a routerLink="/help/configuration">Login and Configuration</a> help section.
</p>

<div class="flex-container">
<div class="flex-item-rounded-card rounded-card-color">
<form
name="saveConfigForm"
(ngSubmit)="saveConfig(this.getActiveConfig(), saveConfigScope, saveConfigName)"
#saveConfigForm="ngForm"
>
<h3>Backup</h3>
<p style="margin-bottom: 16px">
Create a backup of the current active configuration on the server.
</p>
@if (hasToken) {
<div>
<mat-form-field style="width: 25%">
<mat-label>Scope</mat-label>
<mat-select
name="serverConfiScope"
[(ngModel)]="saveConfigScope"
required
>
<mat-option value="global"> Global </mat-option>
<mat-option value="user"> User </mat-option>
</mat-select>
</mat-form-field>
<mat-form-field style="width: 70%; padding-left: 3%">
<mat-label>Configuration Name</mat-label>
<input
matInput
name="serverConfigName"
placeholder="Enter a name for the configuration"
[(ngModel)]="saveConfigName"
[ngModelOptions]="{ standalone: false }"
required
/>
</mat-form-field>
@if (!hasToken) {
<div>
Writing to the server requires "Login to Server" authentication or a
Device token
<h3>Profiles</h3>

@if (profilesAvailable()) {
<mat-divider></mat-divider>
<div class="profile-list">
@for (profile of profiles(); track profile.name) {
<div class="profile-row" [class.active]="profile.isActive">
<div class="profile-name">
@if (profile.isActive) {
<mat-icon class="active-icon" svgIcon="dashboard-dashboard"></mat-icon>
}
<span>{{ profile.name }}</span>
@if (profile.isActive) {
<span class="active-tag">Active on this device</span>
}
</div>
}
</div>
} @else {
<div class="no-token-notice">
<p>Server authentication or Device Token required</p>
</div>
}
<div class="formActionFooter">
<mat-divider class="formActionDivider"></mat-divider>
<button
mat-flat-button
type="submit"
[disabled]="!hasToken || !saveConfigForm.valid"
color="accent"
>
Create
</button>
</div>
</form>
</div>
<div class="flex-item-rounded-card rounded-card-color">
<form
name="deleteConfigForm"
(ngSubmit)="deleteConfigByKey(deleteConfigKey)"
#deleteConfigForm="ngForm"
>
<h3>Delete</h3>
<p style="margin-bottom: 16px">
Select a backup configuration to permanently delete from the server.
</p>
@if (hasToken) {
<div>
<mat-form-field style="width: 100%">
<mat-label>Configuration</mat-label>
<mat-select
name="selectedDeleteItem"
[(ngModel)]="deleteConfigKey"
required
>
@for (config of serverConfigOptions(); track config.key) {
<mat-option
[value]="config.key"
>
{{ config.scope }} / {{ config.name }}
</mat-option>
<div class="profile-actions">
@if (!profile.isActive) {
<button mat-button color="accent" (click)="switchProfile(profile.name)">Switch</button>
}
</mat-select>
</mat-form-field>
</div>
} @else {
<div class="no-token-notice">
<p>Authentication or Device Token required</p>
</div>
}
<div class="formActionFooter">
<mat-divider class="formActionDivider"></mat-divider>
<button
mat-flat-button
type="submit"
[disabled]="!hasToken || !deleteConfigForm.valid"
color="accent"
>
Delete
</button>
<button mat-button [disabled]="!canWriteUserData()" (click)="renameProfile(profile.name)">Rename</button>
<button mat-button [disabled]="!canWriteUserData()" (click)="duplicateProfile(profile.name)">Duplicate</button>
<button
mat-button
color="warn"
[disabled]="profile.isActive || profile.name === 'default' || profiles().length <= 1 || !canWriteUserData()"
(click)="deleteProfile(profile.name)"
>
Delete
</button>
</div>
</div>
} @empty {
<p class="empty-note">No profiles found yet.</p>
}
</div>
</form>
</div>
<div class="flex-item-rounded-card rounded-card-color">
<form [formGroup]="copyConfigForm" (ngSubmit)="copyConfig()">
<h3>Restore</h3>
<p style="margin-bottom: 16px">
Replace the current configuration with a backup from the server.
</p>
@if (hasToken) {
<div>
<mat-form-field style="width: 100%">
<mat-label>Configuration</mat-label>
<mat-select formControlName="sourceTarget">
@for (config of serverConfigOptions(); track config.key) {
<mat-option
[value]="config.key"
>
{{ config.scope }} / {{ config.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
} @else {
<div class="no-token-notice">
<p>Authentication or Device Token required</p>
</div>
}

<div class="formActionFooter">
<mat-divider class="formActionDivider"></mat-divider>
<button
mat-flat-button
type="submit"
color="accent"
[disabled]="!this.copyConfigForm.valid"
>
Restore
</button>
<button mat-flat-button color="accent" [disabled]="!canWriteUserData()" (click)="createProfile()">New</button>
@if (!canWriteUserData()) {
<p class="empty-note">Read-only session — profile changes are disabled.</p>
}
</div>
} @else {
<div class="no-token-notice">
<p>Profiles require logging in to Signal K with a user account.</p>
</div>
</form>
}
</div>

<div class="flex-item-reset rounded-card-color">
<h3>Advanced</h3>
<div class="config-operation-container" style="margin-top: 20px">
<div class="download-txt">
To reuse your configuration on different devices, simply login using the same credential
from any device (See <a routerLink="/help/configuration">Login and Configuration</a> for more information). If
you need to move your configuration between different Signal K servers, you can use Download
to obtain a copy of the active configuration to a file.
Download a copy of the active profile's configuration to a file, for backup or to move it
to another Signal K server.
</div>
<div class="download-btn btn-div">
<button
mat-flat-button
type="button"
class="adv-btn"
(click)="downloadJsonConfig()"
>
Download
</button>
<button mat-flat-button type="button" class="adv-btn" (click)="downloadJsonConfig()">Download</button>
</div>

<div class="upload-txt">
Select a configuration file to upload. The file must be a valid KIP
configuration file of JSON format.
<br /><strong>WARNING: This will permanently overwrite active configuration.</strong>
Import a KIP configuration file as a <strong>new profile</strong>. This never overwrites an
existing profile.
</div>
<div class="upload-btn btn-div">
<!-- Hidden file input -->
<input
type="file"
#fileInput
(change)="uploadJsonConfig($event)"
accept=".json"
hidden
/>
<!-- Material button to trigger file input -->
<button mat-flat-button class="adv-btn" (click)="fileInput.click()">
Upload
</button>
<input type="file" #fileInput (change)="uploadJsonConfig($event)" accept=".json" hidden />
<button mat-flat-button class="adv-btn" [disabled]="!canWriteUserData()" (click)="fileInput.click()">Import</button>
</div>

<div class="demo-txt">
Load the demonstration configuration and connection to Signal K demo
server.
<br /><strong>WARNING: This will permanently reset your active configuration.</strong>
Load the demonstration configuration and connect to the Signal K demo server.
<br /><strong>WARNING: This replaces the active profile's configuration.</strong>
</div>
<div class="demo-btn btn-div">
<button
mat-flat-button
type="button"
class="adv-btn"
(click)="loadDemoConfig()"
>
Demo
</button>
<button mat-flat-button type="button" class="adv-btn" (click)="loadDemoConfig()">Demo</button>
</div>

<div class="reset-txt">
Reset all your settings to default. The default configuration has a
single Getting Started instruction widget. Your Signal K server
connection settings will remain.
<br /><strong>WARNING: This will permanently reset your active configuration.</strong>
Reset the active profile to defaults (a single Getting Started widget). Your Signal K
connection settings remain.
<br /><strong>WARNING: This replaces the active profile's configuration.</strong>
</div>
<div class="reset-btn btn-div">
<button
mat-flat-button
type="button"
class="adv-btn"
(click)="resetConfigToDefault()"
>
Default
</button>
<button mat-flat-button type="button" class="adv-btn" (click)="resetConfigToDefault()">Default</button>
</div>

<div class="config-txt">
Clear the current connection configuration. This only affects
Connectivity settings tab.
Clear the current connection configuration. This only affects the Connectivity settings tab.
</div>
<div class="config-btn btn-div">
<button
mat-flat-button
type="button"
class="adv-btn"
(click)="resetConnectionToDefault()"
>
Connection
</button>
<button mat-flat-button type="button" class="adv-btn" (click)="resetConnectionToDefault()">Connection</button>
</div>
</div>
</div>
Expand Down
Loading