feat(servers/settings): magic string invites, Navidrome admin share, and add-user validation (#258)

* feat(servers): magic string invites and duplicate-aware labels

Add psysonic1- share payloads (URL, Subsonic credentials, optional server
name). Login and add-server forms decode on input and keep magic field
below standard fields. Navidrome User Management generates strings after
PUT password updates where required. Resolve duplicate display names as
username@host in chrome, settings, and login.

* feat(servers): tighten magic-string import and admin share flow

After a decoded magic string, username is read-only and the password field
shows a fixed-length mask so length is not inferred. Password reveal stays
disabled until saved-server quick connect (login) or reopening the add-
server form. Navidrome admin share UI persists the password via API before
copy and adds a plaintext-handling disclaimer. Locales updated.

* feat(settings): save new Navidrome user and copy magic string

Add a non-admin "Save and get magic string" flow: create the user, assign
libraries when needed, then encode credentials to the clipboard. Reuse
the plaintext-handling disclaimer without the password-update hint meant
for edits. Strings added for all locales.

* fix(settings): tighten Navidrome add-user validation and submit feedback

Require username, display name, and a non-empty password (trimmed) for new
users; gate Save and “save and get magic string” on explicit checks with
toast feedback. Show red borders only after a failed submit, and clear
them when the user fills the fields. When editing, keep an empty password
as “unchanged” and validate identity fields with a dedicated message.
Strings: userMgmtValidationMissingIdentity across locales.
This commit is contained in:
cucadmuh
2026-04-22 01:45:28 +03:00
committed by GitHub
parent b61c168430
commit f6f76723d8
16 changed files with 910 additions and 51 deletions
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import {
SERVER_MAGIC_STRING_PREFIX,
DECODED_PASSWORD_VISUAL_MASK,
decodeServerMagicString,
encodeServerMagicString,
} from './serverMagicString';
describe('DECODED_PASSWORD_VISUAL_MASK', () => {
it('has fixed length independent of real passwords', () => {
expect(DECODED_PASSWORD_VISUAL_MASK.length).toBe(10);
});
});
describe('serverMagicString', () => {
it('round-trips url, username, password', () => {
const original = {
url: 'https://music.example.com',
username: 'alice',
password: 's3cret!',
};
const encoded = encodeServerMagicString(original);
expect(encoded.startsWith(SERVER_MAGIC_STRING_PREFIX)).toBe(true);
expect(decodeServerMagicString(encoded)).toEqual(original);
});
it('round-trips optional name', () => {
const original = {
url: 'http://127.0.0.1:4533',
username: 'bob',
password: 'x',
name: 'Home',
};
const encoded = encodeServerMagicString(original);
expect(decodeServerMagicString(encoded)).toEqual(original);
});
it('rejects invalid input', () => {
expect(decodeServerMagicString('')).toBeNull();
expect(decodeServerMagicString('nope')).toBeNull();
expect(decodeServerMagicString(`${SERVER_MAGIC_STRING_PREFIX}%%%`)).toBeNull();
});
});